From 663bf4d3eba16e1d0a781ac5261541e7e2d6b4f2 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Tue, 19 May 2026 22:17:00 +0300 Subject: #4823 WebRTC crashes --- indra/llwebrtc/llwebrtc.cpp | 87 ++++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 41 deletions(-) (limited to 'indra/llwebrtc/llwebrtc.cpp') diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp index a286f75f42..f4ecce63a6 100644 --- a/indra/llwebrtc/llwebrtc.cpp +++ b/indra/llwebrtc/llwebrtc.cpp @@ -841,22 +841,23 @@ void LLWebRTCPeerConnectionImpl::init(LLWebRTCImpl * webrtc_impl) void LLWebRTCPeerConnectionImpl::terminate() { mPendingJobs++; + webrtc::scoped_refptr self(this); mWebRTCImpl->PostSignalingTask( - [this]() + [self]() { - if (mPeerConnection) + if (self->mPeerConnection) { - if (mDataChannel) + if (self->mDataChannel) { { - mDataChannel->Close(); - mDataChannel = nullptr; + self->mDataChannel->Close(); + self->mDataChannel = nullptr; } } // to remove 'Secondlife is recording' icon from taskbar // if user was speaking - auto senders = mPeerConnection->GetSenders(); + auto senders = self->mPeerConnection->GetSenders(); for (auto& sender : senders) { auto track = sender->track(); @@ -866,24 +867,24 @@ void LLWebRTCPeerConnectionImpl::terminate() } } - mPeerConnection->Close(); - if (mLocalStream) + self->mPeerConnection->Close(); + if (self->mLocalStream) { - auto tracks = mLocalStream->GetAudioTracks(); + auto tracks = self->mLocalStream->GetAudioTracks(); for (auto& track : tracks) { - mLocalStream->RemoveTrack(track); + self->mLocalStream->RemoveTrack(track); } - mLocalStream = nullptr; + self->mLocalStream = nullptr; } - mPeerConnection = nullptr; + self->mPeerConnection = nullptr; - for (auto &observer : mSignalingObserverList) + for (auto &observer : self->mSignalingObserverList) { observer->OnPeerConnectionClosed(); } } - mPendingJobs--; + self->mPendingJobs--; }); } @@ -906,8 +907,9 @@ bool LLWebRTCPeerConnectionImpl::initializeConnection(const LLWebRTCPeerConnecti mAnswerReceived = false; mPendingJobs++; + webrtc::scoped_refptr self(this); mWebRTCImpl->PostSignalingTask( - [this,options]() + [self,options]() { webrtc::PeerConnectionInterface::RTCConfiguration config; for (auto server : options.mServers) @@ -926,42 +928,42 @@ bool LLWebRTCPeerConnectionImpl::initializeConnection(const LLWebRTCPeerConnecti config.set_min_port(60000); config.set_max_port(60100); - webrtc::PeerConnectionDependencies pc_dependencies(this); + webrtc::PeerConnectionDependencies pc_dependencies(self.get()); // Other thread manages mPeerConnectionFactory's lifetime and it can be reset // at any momment, create own scoped_refptr (atomic). - webrtc::scoped_refptr peer_connection_factory = mPeerConnectionFactory; + webrtc::scoped_refptr peer_connection_factory = self->mPeerConnectionFactory; if (peer_connection_factory == nullptr) { RTC_LOG(LS_ERROR) << __FUNCTION__ << "Error creating peer connection, factory doesn't exist"; // Too early? - mPendingJobs--; + self->mPendingJobs--; return; } auto error_or_peer_connection = peer_connection_factory->CreatePeerConnectionOrError(config, std::move(pc_dependencies)); if (error_or_peer_connection.ok()) { - mPeerConnection = std::move(error_or_peer_connection.value()); + self->mPeerConnection = std::move(error_or_peer_connection.value()); } else { RTC_LOG(LS_ERROR) << __FUNCTION__ << "Error creating peer connection: " << error_or_peer_connection.error().message(); - for (auto &observer : mSignalingObserverList) + for (auto &observer : self->mSignalingObserverList) { observer->OnRenegotiationNeeded(); } - mPendingJobs--; + self->mPendingJobs--; return; } webrtc::DataChannelInit init; init.ordered = true; - auto data_channel_or_error = mPeerConnection->CreateDataChannelOrError("SLData", &init); + auto data_channel_or_error = self->mPeerConnection->CreateDataChannelOrError("SLData", &init); if (data_channel_or_error.ok()) { - mDataChannel = std::move(data_channel_or_error.value()); + self->mDataChannel = std::move(data_channel_or_error.value()); - mDataChannel->RegisterObserver(this); + self->mDataChannel->RegisterObserver(self.get()); } webrtc::AudioOptions audioOptions; @@ -970,16 +972,16 @@ bool LLWebRTCPeerConnectionImpl::initializeConnection(const LLWebRTCPeerConnecti audioOptions.noise_suppression = true; audioOptions.init_recording_on_send = false; - mLocalStream = peer_connection_factory->CreateLocalMediaStream("SLStream"); + self->mLocalStream = peer_connection_factory->CreateLocalMediaStream("SLStream"); webrtc::scoped_refptr audio_track( peer_connection_factory->CreateAudioTrack("SLAudio", peer_connection_factory->CreateAudioSource(audioOptions).get())); audio_track->set_enabled(false); - mLocalStream->AddTrack(audio_track); + self->mLocalStream->AddTrack(audio_track); - mPeerConnection->AddTrack(audio_track, {"SLStream"}); + self->mPeerConnection->AddTrack(audio_track, {"SLStream"}); - auto senders = mPeerConnection->GetSenders(); + auto senders = self->mPeerConnection->GetSenders(); for (auto &sender : senders) { @@ -995,7 +997,7 @@ bool LLWebRTCPeerConnectionImpl::initializeConnection(const LLWebRTCPeerConnecti sender->SetParameters(params); } - auto receivers = mPeerConnection->GetReceivers(); + auto receivers = self->mPeerConnection->GetReceivers(); for (auto &receiver : receivers) { webrtc::RtpParameters params; @@ -1011,9 +1013,9 @@ bool LLWebRTCPeerConnectionImpl::initializeConnection(const LLWebRTCPeerConnecti } webrtc::PeerConnectionInterface::RTCOfferAnswerOptions offerOptions; - this->AddRef(); // CreateOffer will deref this when it's done. Without this, the callbacks never get called. - mPeerConnection->CreateOffer(this, offerOptions); - mPendingJobs--; + self->AddRef(); // CreateOffer will deref this when it's done. Without this, the callbacks never get called. + self->mPeerConnection->CreateOffer(self.get(), offerOptions); + self->mPendingJobs--; }); return true; @@ -1090,14 +1092,15 @@ void LLWebRTCPeerConnectionImpl::setMute(bool mute) mPendingJobs++; + webrtc::scoped_refptr self(this); mWebRTCImpl->PostSignalingTask( - [this, force_reset, enable]() + [self, force_reset, enable]() { - if (mPeerConnection) + if (self->mPeerConnection) { - auto senders = mPeerConnection->GetSenders(); + auto senders = self->mPeerConnection->GetSenders(); - RTC_LOG(LS_INFO) << __FUNCTION__ << (mMute ? "disabling" : "enabling") << " streams count " << senders.size(); + RTC_LOG(LS_INFO) << __FUNCTION__ << (self->mMute ? "disabling" : "enabling") << " streams count " << senders.size(); for (auto &sender : senders) { auto track = sender->track(); @@ -1113,7 +1116,7 @@ void LLWebRTCPeerConnectionImpl::setMute(bool mute) track->set_enabled(enable); } } - mPendingJobs--; + self->mPendingJobs--; } }); } @@ -1253,12 +1256,14 @@ void LLWebRTCPeerConnectionImpl::OnConnectionChange(webrtc::PeerConnectionInterf case webrtc::PeerConnectionInterface::PeerConnectionState::kConnected: { mPendingJobs++; - mWebRTCImpl->PostWorkerTask([this]() { - for (auto &observer : mSignalingObserverList) + webrtc::scoped_refptr self(this); + mWebRTCImpl->PostWorkerTask([self]() + { + for (auto &observer : self->mSignalingObserverList) { - observer->OnAudioEstablished(this); + observer->OnAudioEstablished(self.get()); } - mPendingJobs--; + self->mPendingJobs--; }); break; } -- cgit v1.3 From 95120bc676fc68793da2d4c06542cf7cb3272d00 Mon Sep 17 00:00:00 2001 From: Roxie Linden Date: Fri, 26 Jun 2026 00:07:52 -0700 Subject: Update libwebrtc to m144 and fix audio device lifecycle/processing - Update libwebrtc to version m144 (autobuild.xml). - Use WebRTC's software APM exclusively; disable built-in (hardware/OS) AEC/AGC/NS, including after each device (re)deploy. - Only run the output device once a peer connection's audio is established (and bring devices up with the user's selected device at that point), fixing the buzz heard before/without an active connection. - Keep capture warm across mute/unmute to avoid the AEC cold-start hiss; stop recording 30s after a sustained mute so the OS mic indicator clears. - Reliably (re)select and (re)start capture/playout after teleport or voice restart so audio sends/records again. - Don't suspend the voice channel when entering tuning mode (Vivox-era behavior that dropped the peer connection). Co-Authored-By: Claude Opus 4.8 (1M context) --- autobuild.xml | 14 +- indra/llwebrtc/llwebrtc.cpp | 294 +++++++++++++++++++++++---- indra/llwebrtc/llwebrtc_impl.h | 52 ++--- indra/newview/llpanelvoicedevicesettings.cpp | 7 +- 4 files changed, 284 insertions(+), 83 deletions(-) (limited to 'indra/llwebrtc/llwebrtc.cpp') diff --git a/autobuild.xml b/autobuild.xml index 1456dca104..5a08e4eeba 100644 --- a/autobuild.xml +++ b/autobuild.xml @@ -2607,11 +2607,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors archive hash - 72ed1f6d469a8ffaffd69be39b7af186d7c3b1d7 + c70247d7683312ee81149dbae603574c0851e04c hash_algorithm sha1 url - https://github.com/secondlife/3p-webrtc-build/releases/download/m137.7151.04.22/webrtc-m137.7151.04.22.21966754211-darwin64-21966754211.tar.zst + https://github.com/secondlife/3p-webrtc-build/releases/download/m144.7559.06.16/webrtc-m144.7559.06.16.28218655958-darwin64-28218655958.tar.zst name darwin64 @@ -2621,11 +2621,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors archive hash - b4d0c836d99491841c3816ff93bb2655a2817bd3 + d187fd666eec8c14dbef959cdc9a6600a13736c7 hash_algorithm sha1 url - https://github.com/secondlife/3p-webrtc-build/releases/download/m137.7151.04.22/webrtc-m137.7151.04.22.21966754211-linux64-21966754211.tar.zst + https://github.com/secondlife/3p-webrtc-build/releases/download/m144.7559.06.16/webrtc-m144.7559.06.16.28218655958-linux64-28218655958.tar.zst name linux64 @@ -2635,11 +2635,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors archive hash - ab2bddd77b1568b22b50ead13c1c33da94f4d59a + 47ecfec6deaa775c958fc532d7a43d186ba191f1 hash_algorithm sha1 url - https://github.com/secondlife/3p-webrtc-build/releases/download/m137.7151.04.22/webrtc-m137.7151.04.22.21966754211-windows64-21966754211.tar.zst + https://github.com/secondlife/3p-webrtc-build/releases/download/m144.7559.06.16/webrtc-m144.7559.06.16.28218655958-windows64-28218655958.tar.zst name windows64 @@ -2652,7 +2652,7 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors copyright Copyright (c) 2011, The WebRTC project authors. All rights reserved. version - m137.7151.04.22.21966754211 + m144.7559.06.16.28218655958 name webrtc vcs_branch diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp index f4ecce63a6..ab455c9645 100644 --- a/indra/llwebrtc/llwebrtc.cpp +++ b/indra/llwebrtc/llwebrtc.cpp @@ -27,7 +27,7 @@ #include "llwebrtc_impl.h" #include #include - +#include "api/audio/create_audio_device_module.h" #include "api/audio_codecs/audio_decoder_factory.h" #include "api/audio_codecs/audio_encoder_factory.h" #include "api/audio_codecs/builtin_audio_decoder_factory.h" @@ -49,6 +49,12 @@ static int16_t PLAYOUT_DEVICE_DEFAULT = 0; static int16_t RECORD_DEVICE_DEFAULT = 0; #endif +// How long to keep the capture device running after a mute before stopping it. +// Keeping capture alive across brief mute/unmute cycles avoids cold-starting +// the AEC (heard as a short hiss on unmute); once the mute has been held this +// long we stop recording so the OS "mic in use" indicator clears. +static const int MUTE_STOP_RECORDING_DELAY_MS = 30000; + // // LLWebRTCAudioTransport implementation @@ -134,7 +140,9 @@ int32_t LLWebRTCAudioTransport::NeedMorePlayData(size_t number_of_frames, if (!engine) { // No engine sink; output silence to be safe. - const size_t bytes = number_of_frames * bytes_per_frame * number_of_channels; + // bytes_per_frame already accounts for all channels, so do not multiply + // by number_of_channels again (that would overrun the playout buffer). + const size_t bytes = number_of_frames * bytes_per_frame; memset(audio_data, 0, bytes); number_of_samples_out = bytes_per_frame; return 0; @@ -250,17 +258,51 @@ void LLCustomProcessor::Process(webrtc::AudioBuffer *audio) mState->setMicrophoneEnergy(std::sqrt(totalSum / (audio->num_channels() * audio->num_frames() * buffer_size))); } + +// +// LLWebRTCImpl implementation +// + +void LLWebRTCAudioDeviceModule::SetTuning(bool tuning, bool mute) +{ + tuning_ = tuning; + if (tuning) + { + int32_t hr = inner_->InitMicrophone(); + hr = inner_->InitRecording(); + hr = inner_->StartRecording(); + hr = inner_->StopPlayout(); + } + else + { + if (mute) + { + inner_->StopRecording(); + } + else + { + inner_->InitRecording(); + inner_->StartRecording(); + } + inner_->StartPlayout(); + } +} + // // LLWebRTCImpl implementation // LLWebRTCImpl::LLWebRTCImpl(LLWebRTCLogCallback* logCallback) : + mEnv(webrtc::CreateEnvironment(webrtc::CreateDefaultTaskQueueFactory())), mLogSink(new LLWebRTCLogSink(logCallback)), mPeerCustomProcessor(nullptr), mMute(true), mTuningMode(false), mDevicesDeploying(0), - mGain(0.0f) + mGain(0.0f), + mBuiltinNS(false), + mBuiltinAGC(false), + mBuiltinAEC(false) { } @@ -273,8 +315,6 @@ void LLWebRTCImpl::init() webrtc::LogMessage::SetLogToStderr(true); webrtc::LogMessage::AddLogToStream(mLogSink, webrtc::LS_VERBOSE); - mTaskQueueFactory = webrtc::CreateDefaultTaskQueueFactory(); - // Create the native threads. mNetworkThread = webrtc::Thread::CreateWithSocketServer(); mNetworkThread->SetName("WebRTCNetworkThread", nullptr); @@ -290,9 +330,17 @@ void LLWebRTCImpl::init() [this]() { webrtc::scoped_refptr realADM = - webrtc::AudioDeviceModule::Create(webrtc::AudioDeviceModule::AudioLayer::kPlatformDefaultAudio, mTaskQueueFactory.get()); + webrtc::CreateAudioDeviceModule(mEnv, webrtc::AudioDeviceModule::AudioLayer::kPlatformDefaultAudio); mDeviceModule = webrtc::make_ref_counted(realADM); mDeviceModule->SetObserver(this); + mDeviceModule->Init(); + + mBuiltinNS = mDeviceModule->BuiltInNSIsAvailable(); + mBuiltinAEC = mDeviceModule->BuiltInAECIsAvailable(); + mBuiltinAGC = mDeviceModule->BuiltInAGCIsAvailable(); + // All audio processing is done by WebRTC's software APM (configured + // below); make sure the hardware processors stay off. + workerDisableBuiltInAudioProcessing(); }); // The custom processor allows us to retrieve audio data (and levels) @@ -302,17 +350,22 @@ void LLWebRTCImpl::init() apb.SetCapturePostProcessing(std::make_unique(mPeerCustomProcessor)); mAudioProcessingModule = apb.Build(webrtc::CreateEnvironment()); + // Initial software-APM state, matching setAudioConfig() so there's no + // window where processing differs before the viewer's first config call. + // All processing is done here in software (the hardware AEC/AGC/NS is kept + // disabled), so enable echo cancellation from the very first frame. webrtc::AudioProcessing::Config apm_config; - apm_config.echo_canceller.enabled = false; - apm_config.echo_canceller.mobile_mode = false; - apm_config.gain_controller1.enabled = false; - apm_config.gain_controller2.enabled = true; - apm_config.high_pass_filter.enabled = true; - apm_config.noise_suppression.enabled = true; - apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kVeryHigh; - apm_config.transient_suppression.enabled = true; - apm_config.pipeline.multi_channel_render = true; - apm_config.pipeline.multi_channel_capture = false; + apm_config.echo_canceller.enabled = true; + apm_config.echo_canceller.mobile_mode = false; + apm_config.gain_controller1.enabled = false; + apm_config.gain_controller2.enabled = true; + apm_config.gain_controller2.adaptive_digital.enabled = true; // auto-level speech + apm_config.high_pass_filter.enabled = true; + apm_config.noise_suppression.enabled = true; + apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kVeryHigh; + apm_config.transient_suppression.enabled = true; + apm_config.pipeline.multi_channel_render = true; + apm_config.pipeline.multi_channel_capture = true; mAudioProcessingModule->ApplyConfig(apm_config); @@ -344,7 +397,6 @@ void LLWebRTCImpl::init() { if (mDeviceModule) { - mDeviceModule->EnableBuiltInAEC(false); updateDevices(); } }); @@ -382,7 +434,6 @@ void LLWebRTCImpl::terminate() mDeviceModule->Terminate(); } mDeviceModule = nullptr; - mTaskQueueFactory = nullptr; }); // In case peer connections still somehow have jobs in workers, @@ -395,47 +446,79 @@ void LLWebRTCImpl::terminate() webrtc::LogMessage::RemoveLogToStream(mLogSink); } + void LLWebRTCImpl::setAudioConfig(LLWebRTCDeviceInterface::AudioConfig config) { + // All audio processing is handled by WebRTC's software APM here. The + // platform/hardware AEC/AGC/NS is always disabled (see + // workerDisableBuiltInAudioProcessing), so these are enabled purely on the + // requested config without deferring to any built-in processor. webrtc::AudioProcessing::Config apm_config; - apm_config.echo_canceller.enabled = config.mEchoCancellation; - apm_config.echo_canceller.mobile_mode = false; - apm_config.gain_controller1.enabled = false; - apm_config.gain_controller2.enabled = config.mAGC; + apm_config.echo_canceller.enabled = config.mEchoCancellation; + apm_config.echo_canceller.mobile_mode = false; + apm_config.gain_controller1.enabled = false; + apm_config.gain_controller2.enabled = config.mAGC; apm_config.gain_controller2.adaptive_digital.enabled = true; // auto-level speech - apm_config.high_pass_filter.enabled = true; - apm_config.transient_suppression.enabled = true; - apm_config.pipeline.multi_channel_render = true; - apm_config.pipeline.multi_channel_capture = true; - apm_config.pipeline.multi_channel_capture = true; + apm_config.high_pass_filter.enabled = true; + apm_config.transient_suppression.enabled = true; + apm_config.pipeline.multi_channel_render = true; + apm_config.pipeline.multi_channel_capture = true; switch (config.mNoiseSuppressionLevel) { case LLWebRTCDeviceInterface::AudioConfig::NOISE_SUPPRESSION_LEVEL_NONE: apm_config.noise_suppression.enabled = false; - apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kLow; + apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kLow; break; case LLWebRTCDeviceInterface::AudioConfig::NOISE_SUPPRESSION_LEVEL_LOW: apm_config.noise_suppression.enabled = true; - apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kLow; + apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kLow; break; case LLWebRTCDeviceInterface::AudioConfig::NOISE_SUPPRESSION_LEVEL_MODERATE: apm_config.noise_suppression.enabled = true; - apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kModerate; + apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kModerate; break; case LLWebRTCDeviceInterface::AudioConfig::NOISE_SUPPRESSION_LEVEL_HIGH: apm_config.noise_suppression.enabled = true; - apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kHigh; + apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kHigh; break; case LLWebRTCDeviceInterface::AudioConfig::NOISE_SUPPRESSION_LEVEL_VERY_HIGH: apm_config.noise_suppression.enabled = true; - apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kVeryHigh; + apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kVeryHigh; break; default: apm_config.noise_suppression.enabled = false; - apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kLow; + apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kLow; } mAudioProcessingModule->ApplyConfig(apm_config); + + // Keep the hardware processors off; the APM above is the only processing. + PostWorkerTask([this]() { workerDisableBuiltInAudioProcessing(); }); +} + +void LLWebRTCImpl::workerDisableBuiltInAudioProcessing() +{ + if (!mDeviceModule) + { + return; + } + + // We always use WebRTC's internal (software APM) audio processing. Running + // the platform/hardware AEC, AGC, or NS alongside it causes the two to + // fight -- pumping levels, double noise suppression, and mismatched AEC + // references -- so disable any that the device exposes. + if (mBuiltinNS) + { + mDeviceModule->EnableBuiltInNS(false); + } + if (mBuiltinAGC) + { + mDeviceModule->EnableBuiltInAGC(false); + } + if (mBuiltinAEC) + { + mDeviceModule->EnableBuiltInAEC(false); + } } void LLWebRTCImpl::refreshDevices() @@ -455,8 +538,11 @@ void LLWebRTCImpl::unsetDevicesObserver(LLWebRTCDevicesObserver *observer) } } -// must be run in the worker thread. -void LLWebRTCImpl::workerDeployDevices() +// must be run in the worker thread. Selects the user's chosen capture/playout +// devices and (re)initializes and starts them. Does NOT touch per-connection +// tracks -- callers that also need mute/track state re-applied use +// workerDeployDevices(). +void LLWebRTCImpl::workerStartDevices() { if (!mDeviceModule) { @@ -500,8 +586,20 @@ void LLWebRTCImpl::workerDeployDevices() #endif mDeviceModule->InitMicrophone(); mDeviceModule->SetStereoRecording(false); + mBuiltinNS = mDeviceModule->BuiltInNSIsAvailable(); + mBuiltinAEC = mDeviceModule->BuiltInAECIsAvailable(); + mBuiltinAGC = mDeviceModule->BuiltInAGCIsAvailable(); + // A newly-selected capture device may default its hardware AEC/AGC/NS on; + // disable before InitRecording so the recording stream is configured to + // use only WebRTC's software APM. + workerDisableBuiltInAudioProcessing(); mDeviceModule->InitRecording(); + if ((!mMute && mPeerConnections.size()) || mTuningMode) + { + mDeviceModule->ForceStartRecording(); + } + int16_t playoutDevice = PLAYOUT_DEVICE_DEFAULT; int16_t playout_device_start = 0; if (mPlayoutDevice != "Default") @@ -538,15 +636,30 @@ void LLWebRTCImpl::workerDeployDevices() mDeviceModule->SetStereoPlayout(true); mDeviceModule->InitPlayout(); - if ((!mMute && mPeerConnections.size()) || mTuningMode) + // Only run playout when there's actually something to render. Starting + // playout with no peer connection leaves the output device spinning with + // no engine data, which is heard as a buzz until a connection is made. + // (Recording is gated on the same condition above.) + if (!mTuningMode && !mPeerConnections.empty()) { - mDeviceModule->ForceStartRecording(); + mDeviceModule->StartPlayout(); } +} - if (!mTuningMode) +// must be run in the worker thread. Selects/starts the devices (via +// workerStartDevices) and then re-applies per-connection mute/track state. +// Use this for device changes and tuning; for simply bringing devices up when +// a connection is established (without disturbing the connection's own +// mute/track management) call workerStartDevices() directly. +void LLWebRTCImpl::workerDeployDevices() +{ + if (!mDeviceModule) { - mDeviceModule->StartPlayout(); + return; } + + workerStartDevices(); + mSignalingThread->PostTask( [this] { @@ -740,6 +853,12 @@ void LLWebRTCImpl::intSetMute(bool mute, int delay_ms) if (mMute) { + // Keep capturing for a while after muting so quick mute/unmute cycles + // don't cold-start the AEC (and any OS capture effect such as Windows + // Voice Clarity), which is heard as a short hiss on unmute. Once the + // mute has been held this long, stop recording so the OS "mic in use" + // indicator clears. If the user unmutes or toggles before this fires, + // the sequence check turns it into a no-op and capture keeps running. mWorkerThread->PostDelayedTask( [this, current_sequence] { @@ -748,7 +867,7 @@ void LLWebRTCImpl::intSetMute(bool mute, int delay_ms) mDeviceModule->ForceStopRecording(); } }, - webrtc::TimeDelta::Millis(delay_ms)); + webrtc::TimeDelta::Millis(MUTE_STOP_RECORDING_DELAY_MS)); } else { @@ -757,6 +876,9 @@ void LLWebRTCImpl::intSetMute(bool mute, int delay_ms) { if (mDeviceModule && (current_sequence == mute_sequence.load())) { + // No-op if capture is still running (the common case, when + // unmuting within the stop delay -> no AEC cold start); + // restarts capture if a sustained mute had stopped it. mDeviceModule->InitRecording(); mDeviceModule->ForceStartRecording(); } @@ -770,8 +892,7 @@ void LLWebRTCImpl::intSetMute(bool mute, int delay_ms) LLWebRTCPeerConnectionInterface *LLWebRTCImpl::newPeerConnection() { - bool empty = mPeerConnections.empty(); - webrtc::scoped_refptr peerConnection = webrtc::scoped_refptr(new webrtc::RefCountedObject()); + webrtc::scoped_refptr peerConnection = webrtc::scoped_refptr(new webrtc::RefCountedObject(mEnv)); peerConnection->init(this); if (mPeerConnections.empty()) { @@ -779,6 +900,13 @@ LLWebRTCPeerConnectionInterface *LLWebRTCImpl::newPeerConnection() } mPeerConnections.emplace_back(peerConnection); + // The capture/playout devices are intentionally NOT started here. This + // runs when the connection is created/connecting; starting the output + // device now leaves it spinning with no decoded audio during the handshake, + // which is heard as a buzz. The devices are (re)started from + // OnConnectionChange(kConnected) instead, once audio is actually + // established (see startAudioDevices()). + peerConnection->enableSenderTracks(false); peerConnection->resetMute(); return peerConnection.get(); @@ -795,10 +923,82 @@ void LLWebRTCImpl::freePeerConnection(LLWebRTCPeerConnectionInterface* peer_conn if (mPeerConnections.empty()) { intSetMute(true); + // Last connection gone: stop capture immediately rather than + // waiting out the mute stop-delay, so the mic isn't held open after + // the call, and stop playout so the output device isn't left + // spinning with no engine data. + mWorkerThread->PostTask( + [this]() + { + if (mDeviceModule) + { + mDeviceModule->ForceStopRecording(); + mDeviceModule->StopPlayout(); + } + }); } } } +void LLWebRTCImpl::startAudioDevices() +{ + // Called when a connection's audio is established. This is the + // authoritative point that brings the devices back (with the user's + // selected devices applied) after all connections dropped (teleport, voice + // restart) or for the first call of a session. It matters because the + // WebRTC engine no-ops Start/StopRecording on our ADM wrapper -- only our + // explicit Force* calls actually drive capture -- so when the devices were + // stopped, nothing else will restart them. + // + // It's guarded on Playing()/Recording() so a second connection establishing + // won't glitch an already-running stream, and doing this at "connected" + // rather than at connection creation avoids running the output device with + // no decoded audio during the handshake. + mWorkerThread->PostTask( + [this]() + { + if (!mDeviceModule || mTuningMode) + { + return; + } + + if (!mDeviceModule->Playing()) + { + // First established connection for this call: select and start + // the user's *chosen* capture/playout devices + // (SetRecordingDevice/SetPlayoutDevice). Just calling + // InitPlayout/InitRecording here would bring the devices up on + // whatever the ADM currently has selected -- the system default + // after a cold start -- which is why a p2p call (or a call after + // teleport/voice-restart) could come up on the wrong device. + // + // We call workerStartDevices() rather than the full + // deployDevices() on purpose: deployDevices() also re-applies + // per-connection mute/track state, which races with the + // viewer's own mute setup for the freshly-establishing + // connection and can leave the sender track disabled (recording + // runs but nothing transmits after teleport). + workerStartDevices(); + } + + // Authoritatively (re)start capture whenever we're connected and not + // device-muted. This runs unconditionally -- NOT just in an else + // branch -- because workerStartDevices() above stops recording while + // re-selecting the device and only restarts it behind a gate; if + // that gate doesn't line up (or capture was stopped on a prior + // disconnect, e.g. teleport), this is what reliably brings the mic + // back. No-op if capture is already running. + if (!mMute && !mPeerConnections.empty() && !mDeviceModule->Recording()) + { + if (!mDeviceModule->RecordingIsInitialized()) + { + mDeviceModule->InitRecording(); + } + mDeviceModule->ForceStartRecording(); + } + }); +} + // // LLWebRTCPeerConnectionImpl implementation. @@ -806,7 +1006,8 @@ void LLWebRTCImpl::freePeerConnection(LLWebRTCPeerConnectionInterface* peer_conn // Most peer connection (signaling) happens on // the signaling thread. -LLWebRTCPeerConnectionImpl::LLWebRTCPeerConnectionImpl() : +LLWebRTCPeerConnectionImpl::LLWebRTCPeerConnectionImpl(const webrtc::Environment& env) : + mEnv(env), mWebRTCImpl(nullptr), mPeerConnection(nullptr), mMute(MUTE_INITIAL), @@ -1255,6 +1456,12 @@ void LLWebRTCPeerConnectionImpl::OnConnectionChange(webrtc::PeerConnectionInterf { case webrtc::PeerConnectionInterface::PeerConnectionState::kConnected: { + // Audio is established now -- (re)start the capture and playout + // devices. Doing this here rather than at connection creation + // avoids running the output device during the handshake (heard as a + // buzz), and reliably restores the devices after a full teardown + // (teleport / voice restart). + mWebRTCImpl->startAudioDevices(); mPendingJobs++; webrtc::scoped_refptr self(this); mWebRTCImpl->PostWorkerTask([self]() @@ -1267,6 +1474,7 @@ void LLWebRTCPeerConnectionImpl::OnConnectionChange(webrtc::PeerConnectionInterf }); break; } + case webrtc::PeerConnectionInterface::PeerConnectionState::kFailed: { for (auto &observer : mSignalingObserverList) diff --git a/indra/llwebrtc/llwebrtc_impl.h b/indra/llwebrtc/llwebrtc_impl.h index bd7a2e0bcf..cfb0d10c29 100644 --- a/indra/llwebrtc/llwebrtc_impl.h +++ b/indra/llwebrtc/llwebrtc_impl.h @@ -323,30 +323,8 @@ public: // tuning microphone energy calculations float GetMicrophoneEnergy() { return audio_transport_.GetMicrophoneEnergy(); } void SetTuningMicGain(float gain) { audio_transport_.SetGain(gain); } - void SetTuning(bool tuning, bool mute) - { - tuning_ = tuning; - if (tuning) - { - inner_->InitRecording(); - inner_->StartRecording(); - inner_->StopPlayout(); - } - else - { - if (mute) - { - inner_->StopRecording(); - } - else - { - inner_->InitRecording(); - inner_->StartRecording(); - } - inner_->InitPlayout(); - inner_->StartPlayout(); - } - } + + void SetTuning(bool tuning, bool mute); protected: ~LLWebRTCAudioDeviceModule() override = default; @@ -436,7 +414,6 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO // void setAudioConfig(LLWebRTCDeviceInterface::AudioConfig config = LLWebRTCDeviceInterface::AudioConfig()) override; - void refreshDevices() override; void setDevicesObserver(LLWebRTCDevicesObserver *observer) override; @@ -522,9 +499,22 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO LLWebRTCPeerConnectionInterface* newPeerConnection(); void freePeerConnection(LLWebRTCPeerConnectionInterface* peer_connection); + // (Re)start the capture and playout devices once a connection's audio is + // established. This is the authoritative point for bringing the devices + // back after all connections dropped (teleport, voice restart). Idempotent + // and safe to call from any thread (work is posted to the worker thread). + void startAudioDevices(); + protected: + const webrtc::Environment mEnv; + void workerStartDevices(); void workerDeployDevices(); + // We always rely on WebRTC's internal (software APM) audio processing, so + // any platform/hardware AEC/AGC/NS must be kept disabled. + void workerDisableBuiltInAudioProcessing(); + + LLWebRTCLogSink* mLogSink; // The native webrtc threads @@ -537,10 +527,6 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO webrtc::scoped_refptr mAudioProcessingModule; - // more native webrtc stuff - std::unique_ptr mTaskQueueFactory; - - // Devices void updateDevices(); void deployDevices(); @@ -548,6 +534,10 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO webrtc::scoped_refptr mDeviceModule; std::vector mVoiceDevicesObserverList; + bool mBuiltinNS; + bool mBuiltinAGC; + bool mBuiltinAEC; + // accessors in native webrtc for devices aren't apparently implemented yet. bool mTuningMode; std::string mRecordingDevice; @@ -580,7 +570,7 @@ class LLWebRTCPeerConnectionImpl : public LLWebRTCPeerConnectionInterface, { public: - LLWebRTCPeerConnectionImpl(); + LLWebRTCPeerConnectionImpl(const webrtc::Environment& env); ~LLWebRTCPeerConnectionImpl(); void init(LLWebRTCImpl * webrtc_impl); @@ -659,7 +649,7 @@ class LLWebRTCPeerConnectionImpl : public LLWebRTCPeerConnectionInterface, void gatherConnectionStats() override; protected: - + const webrtc::Environment mEnv; LLWebRTCImpl * mWebRTCImpl; webrtc::scoped_refptr mPeerConnectionFactory; diff --git a/indra/newview/llpanelvoicedevicesettings.cpp b/indra/newview/llpanelvoicedevicesettings.cpp index d8d6bcf5fd..5aaa53b732 100644 --- a/indra/newview/llpanelvoicedevicesettings.cpp +++ b/indra/newview/llpanelvoicedevicesettings.cpp @@ -338,8 +338,12 @@ void LLPanelVoiceDeviceSettings::initialize() // put voice client in "tuning" mode if (mUseTuningMode) { + // WebRTC tuning only affects the local audio device (mic-level + // monitoring and device selection); the peer connection stays up and + // its send/receive tracks are disabled for the duration. Unlike Vivox, + // there's no need to suspend (and tear down) the voice channel, which + // previously dropped the call and failed to reconnect on resume. LLVoiceClient::getInstance()->tuningStart(); - LLVoiceChannel::suspend(); } } @@ -348,7 +352,6 @@ void LLPanelVoiceDeviceSettings::cleanup() if (mUseTuningMode) { LLVoiceClient::getInstance()->tuningStop(); - LLVoiceChannel::resume(); } } -- cgit v1.3 From 4124f969ee7bbd4e0734ef0d118c60125d7bb358 Mon Sep 17 00:00:00 2001 From: Roxie Linden Date: Tue, 30 Jun 2026 11:48:27 -0700 Subject: Keep capture device running for the whole voice session Mute now zeroes captured gain and disables the sender tracks instead of stopping the capture device, so unmuting no longer cold-starts the AEC (no hiss) and Bluetooth devices no longer drop/restart as they switch between mono and stereo. Capture is gated on voice being enabled rather than on mute: it starts when voice is enabled and runs across calls and mute/unmute, and is released when voice is disabled (setVoiceEnabled). Playout stays gated on there being a connection to render. As a result the OS "mic in use" indicator is on for the length of the session and only clears when voice is disabled. Co-Authored-By: Claude Opus 4.8 (1M context) --- indra/llwebrtc/llwebrtc.cpp | 292 ++++++++++++++++++---------------------- indra/llwebrtc/llwebrtc.h | 8 ++ indra/llwebrtc/llwebrtc_impl.h | 27 ++-- indra/newview/llvoicewebrtc.cpp | 8 ++ 4 files changed, 163 insertions(+), 172 deletions(-) (limited to 'indra/llwebrtc/llwebrtc.cpp') diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp index ab455c9645..6c809f2743 100644 --- a/indra/llwebrtc/llwebrtc.cpp +++ b/indra/llwebrtc/llwebrtc.cpp @@ -49,12 +49,6 @@ static int16_t PLAYOUT_DEVICE_DEFAULT = 0; static int16_t RECORD_DEVICE_DEFAULT = 0; #endif -// How long to keep the capture device running after a mute before stopping it. -// Keeping capture alive across brief mute/unmute cycles avoids cold-starting -// the AEC (heard as a short hiss on unmute); once the mute has been held this -// long we stop recording so the OS "mic in use" indicator clears. -static const int MUTE_STOP_RECORDING_DELAY_MS = 30000; - // // LLWebRTCAudioTransport implementation @@ -268,24 +262,19 @@ void LLWebRTCAudioDeviceModule::SetTuning(bool tuning, bool mute) tuning_ = tuning; if (tuning) { - int32_t hr = inner_->InitMicrophone(); - hr = inner_->InitRecording(); - hr = inner_->StartRecording(); - hr = inner_->StopPlayout(); - } - else - { - if (mute) - { - inner_->StopRecording(); - } - else - { - inner_->InitRecording(); - inner_->StartRecording(); - } - inner_->StartPlayout(); + // Ensure capture is running (it's normally already running -- capture is + // session-long) so the mic-level meter works, and stop rendering the + // call while tuning. The recording calls are no-ops if capture is + // already active, so this won't cold-start it. + inner_->InitMicrophone(); + inner_->InitRecording(); + inner_->StartRecording(); + inner_->StopPlayout(); } + // On exit, capture is deliberately left running (mute is handled by gain, + // not by stopping the device, so there's no AEC cold-start hiss). Playout + // is restored by the caller via workerOpenPlayout(), keeping it gated on + // there being a connection to render. } // @@ -297,6 +286,7 @@ LLWebRTCImpl::LLWebRTCImpl(LLWebRTCLogCallback* logCallback) : mLogSink(new LLWebRTCLogSink(logCallback)), mPeerCustomProcessor(nullptr), mMute(true), + mVoiceEnabled(false), mTuningMode(false), mDevicesDeploying(0), mGain(0.0f), @@ -431,7 +421,7 @@ void LLWebRTCImpl::terminate() { if (mDeviceModule) { - mDeviceModule->Terminate(); + mDeviceModule->ForceTerminate(); } mDeviceModule = nullptr; }); @@ -538,23 +528,25 @@ void LLWebRTCImpl::unsetDevicesObserver(LLWebRTCDevicesObserver *observer) } } -// must be run in the worker thread. Selects the user's chosen capture/playout -// devices and (re)initializes and starts them. Does NOT touch per-connection -// tracks -- callers that also need mute/track state re-applied use -// workerDeployDevices(). -void LLWebRTCImpl::workerStartDevices() +// must be run in the worker thread. Selects the configured capture device and +// starts recording. Capture runs the whole time voice is enabled (it's never +// stopped for mute or between calls, so the AEC never cold-starts -- there's no +// hiss on unmute), so this is a no-op when already recording. Device changes +// go through workerDeployDevices(), which stops recording first to force a +// clean re-select; voice off goes through setVoiceEnabled(false). +void LLWebRTCImpl::workerStartRecording() { - if (!mDeviceModule) + // Only run capture while voice is enabled, and never cold-start it when + // it's already running (that would cause the unmute hiss). + if (!mDeviceModule || !mVoiceEnabled || mDeviceModule->Recording()) { return; } int16_t recordingDevice = RECORD_DEVICE_DEFAULT; - int16_t recording_device_start = 0; - if (mRecordingDevice != "Default") { - for (int16_t i = recording_device_start; i < mRecordingDeviceList.size(); i++) + for (int16_t i = 0; i < mRecordingDeviceList.size(); i++) { if (mRecordingDeviceList[i].mID == mRecordingDevice) { @@ -570,8 +562,6 @@ void LLWebRTCImpl::workerStartDevices() } } - mDeviceModule->StopPlayout(); - mDeviceModule->ForceStopRecording(); #if WEBRTC_WIN if (recordingDevice < 0) { @@ -586,25 +576,32 @@ void LLWebRTCImpl::workerStartDevices() #endif mDeviceModule->InitMicrophone(); mDeviceModule->SetStereoRecording(false); - mBuiltinNS = mDeviceModule->BuiltInNSIsAvailable(); - mBuiltinAEC = mDeviceModule->BuiltInAECIsAvailable(); - mBuiltinAGC = mDeviceModule->BuiltInAGCIsAvailable(); // A newly-selected capture device may default its hardware AEC/AGC/NS on; // disable before InitRecording so the recording stream is configured to // use only WebRTC's software APM. workerDisableBuiltInAudioProcessing(); mDeviceModule->InitRecording(); + mDeviceModule->ForceStartRecording(); +} - if ((!mMute && mPeerConnections.size()) || mTuningMode) +// must be run in the worker thread. Selects the configured playout device and +// starts playout. Playout only runs while there's a connection to render +// (running the output device with no engine data is heard as a buzz), so this +// is a no-op when there are no connections or when already playing. Device +// changes go through workerDeployDevices(), which stops playout first. +void LLWebRTCImpl::workerStartPlayout() +{ + // Only run playout while voice is enabled and there's a connection to + // render (running the output device otherwise is heard as a buzz). + if (!mDeviceModule || !mVoiceEnabled || mTuningMode || mDeviceModule->Playing() || mPeerConnections.empty()) { - mDeviceModule->ForceStartRecording(); + return; } int16_t playoutDevice = PLAYOUT_DEVICE_DEFAULT; - int16_t playout_device_start = 0; if (mPlayoutDevice != "Default") { - for (int16_t i = playout_device_start; i < mPlayoutDeviceList.size(); i++) + for (int16_t i = 0; i < mPlayoutDeviceList.size(); i++) { if (mPlayoutDeviceList[i].mID == mPlayoutDevice) { @@ -635,22 +632,14 @@ void LLWebRTCImpl::workerStartDevices() mDeviceModule->InitSpeaker(); mDeviceModule->SetStereoPlayout(true); mDeviceModule->InitPlayout(); - - // Only run playout when there's actually something to render. Starting - // playout with no peer connection leaves the output device spinning with - // no engine data, which is heard as a buzz until a connection is made. - // (Recording is gated on the same condition above.) - if (!mTuningMode && !mPeerConnections.empty()) - { - mDeviceModule->StartPlayout(); - } + mDeviceModule->StartPlayout(); } -// must be run in the worker thread. Selects/starts the devices (via -// workerStartDevices) and then re-applies per-connection mute/track state. -// Use this for device changes and tuning; for simply bringing devices up when -// a connection is established (without disturbing the connection's own -// mute/track management) call workerStartDevices() directly. +// must be run in the worker thread. Used for device changes and tuning: forces +// a clean re-select of both devices, then re-applies per-connection mute/track +// state. To merely bring playout up when a connection is established (without +// disturbing the connection's own mute/track management) call +// workerOpenPlayout() directly -- see startPlayout(). void LLWebRTCImpl::workerDeployDevices() { if (!mDeviceModule) @@ -658,7 +647,13 @@ void LLWebRTCImpl::workerDeployDevices() return; } - workerStartDevices(); + // Stop first so the start helpers (which no-op when already running) will + // re-select the now-current device. + mDeviceModule->StopPlayout(); + mDeviceModule->ForceStopRecording(); + + workerStartRecording(); + workerStartPlayout(); mSignalingThread->PostTask( [this] @@ -701,6 +696,35 @@ void LLWebRTCImpl::setRenderDevice(const std::string &id) } } +void LLWebRTCImpl::setVoiceEnabled(bool enable) +{ + mVoiceEnabled = enable; + mWorkerThread->PostTask( + [this, enable]() + { + if (!mDeviceModule) + { + return; + } + if (enable) + { + // Voice on: start the capture device (it then stays running + // across calls and mute/unmute), and start playout if there's + // already a connection to render. + mDeviceModule->Init(); + workerDeployDevices(); + } + else + { + // Voice off: release both devices so the OS mic/speaker aren't + // held open. + mDeviceModule->ForceStopRecording(); + mDeviceModule->StopPlayout(); + mDeviceModule->ForceTerminate(); + } + }); +} + // updateDevices needs to happen on the worker thread. void LLWebRTCImpl::updateDevices() { @@ -749,6 +773,8 @@ void LLWebRTCImpl::updateDevices() { observer->OnDevicesChanged(mPlayoutDeviceList, mRecordingDeviceList); } + + deployDevices(); } void LLWebRTCImpl::OnDevicesUpdated() @@ -771,6 +797,13 @@ void LLWebRTCImpl::setTuningMode(bool enable) [this] { mDeviceModule->SetTuning(mTuningMode, mMute); + if (!mTuningMode) + { + // Restore playout after tuning, gated on there being a + // connection to render (so the output device isn't left + // spinning with no engine data). + workerStartPlayout(); + } mSignalingThread->PostTask( [this] { @@ -842,48 +875,16 @@ void LLWebRTCImpl::setMute(bool mute, int delay_ms) void LLWebRTCImpl::intSetMute(bool mute, int delay_ms) { + // Mute by zeroing the captured (post-APM) gain; the sender track is also + // disabled per connection (see LLWebRTCPeerConnectionImpl::setMute). The + // capture device deliberately stays running for the whole session, so + // muting/unmuting never stops or starts it -- that's what avoids the AEC + // cold-start hiss on unmute. Capture start/stop is tied to device + // selection (workerStartRecording) and shutdown, not to mute. if (mPeerCustomProcessor) { mPeerCustomProcessor->setGain(mMute ? 0.0f : mGain); } - - // Sequence counter to prevent race conditions from rapid requests to mute/unmute - static std::atomic mute_sequence(0); - uint32_t current_sequence = ++mute_sequence; - - if (mMute) - { - // Keep capturing for a while after muting so quick mute/unmute cycles - // don't cold-start the AEC (and any OS capture effect such as Windows - // Voice Clarity), which is heard as a short hiss on unmute. Once the - // mute has been held this long, stop recording so the OS "mic in use" - // indicator clears. If the user unmutes or toggles before this fires, - // the sequence check turns it into a no-op and capture keeps running. - mWorkerThread->PostDelayedTask( - [this, current_sequence] - { - if (mDeviceModule && (current_sequence == mute_sequence.load())) - { - mDeviceModule->ForceStopRecording(); - } - }, - webrtc::TimeDelta::Millis(MUTE_STOP_RECORDING_DELAY_MS)); - } - else - { - mWorkerThread->PostTask( - [this, current_sequence] - { - if (mDeviceModule && (current_sequence == mute_sequence.load())) - { - // No-op if capture is still running (the common case, when - // unmuting within the stop delay -> no AEC cold start); - // restarts capture if a sustained mute had stopped it. - mDeviceModule->InitRecording(); - mDeviceModule->ForceStartRecording(); - } - }); - } } // @@ -900,12 +901,12 @@ LLWebRTCPeerConnectionInterface *LLWebRTCImpl::newPeerConnection() } mPeerConnections.emplace_back(peerConnection); - // The capture/playout devices are intentionally NOT started here. This - // runs when the connection is created/connecting; starting the output - // device now leaves it spinning with no decoded audio during the handshake, - // which is heard as a buzz. The devices are (re)started from - // OnConnectionChange(kConnected) instead, once audio is actually - // established (see startAudioDevices()). + // Playout is intentionally NOT started here. This runs when the connection + // is created/connecting; starting the output device now leaves it spinning + // with no decoded audio during the handshake, which is heard as a buzz. + // Playout is started from OnConnectionChange(kConnected) instead, once audio + // is actually established (see startPlayout()). Capture follows + // voice-enabled state, so it's not touched here either. peerConnection->enableSenderTracks(false); peerConnection->resetMute(); @@ -923,79 +924,42 @@ void LLWebRTCImpl::freePeerConnection(LLWebRTCPeerConnectionInterface* peer_conn if (mPeerConnections.empty()) { intSetMute(true); - // Last connection gone: stop capture immediately rather than - // waiting out the mute stop-delay, so the mic isn't held open after - // the call, and stop playout so the output device isn't left - // spinning with no engine data. + // Last connection gone: stop playout (there's nothing to render). + // Capture stays running while voice is enabled so it's ready -- with + // no cold-start hiss -- when the next call comes up. But if voice + // has been disabled, stop capture now: setVoiceEnabled(false) tried + // to, but the engine's send stream was still active then (and the + // engine's own StopRecording is intentionally a no-op), so the stop + // only sticks once the connection -- and its stream -- is gone. mWorkerThread->PostTask( [this]() { if (mDeviceModule) { - mDeviceModule->ForceStopRecording(); mDeviceModule->StopPlayout(); + if (!mVoiceEnabled) + { + mDeviceModule->ForceStopRecording(); + } } }); } } } -void LLWebRTCImpl::startAudioDevices() +void LLWebRTCImpl::startPlayout() { - // Called when a connection's audio is established. This is the - // authoritative point that brings the devices back (with the user's - // selected devices applied) after all connections dropped (teleport, voice - // restart) or for the first call of a session. It matters because the - // WebRTC engine no-ops Start/StopRecording on our ADM wrapper -- only our - // explicit Force* calls actually drive capture -- so when the devices were - // stopped, nothing else will restart them. - // - // It's guarded on Playing()/Recording() so a second connection establishing - // won't glitch an already-running stream, and doing this at "connected" - // rather than at connection creation avoids running the output device with - // no decoded audio during the handshake. + // Called when a connection's audio is established. Only playout is started + // here: it's gated on there being a connection to render, because running + // the output device with no engine data is heard as a buzz. Capture is + // NOT touched here -- it follows voice-enabled state (setVoiceEnabled), so + // it's already running if voice is on and must stay off if voice is off. + // Starting it here would also let a stray kConnected during voice-disable + // teardown re-open the mic. mWorkerThread->PostTask( [this]() { - if (!mDeviceModule || mTuningMode) - { - return; - } - - if (!mDeviceModule->Playing()) - { - // First established connection for this call: select and start - // the user's *chosen* capture/playout devices - // (SetRecordingDevice/SetPlayoutDevice). Just calling - // InitPlayout/InitRecording here would bring the devices up on - // whatever the ADM currently has selected -- the system default - // after a cold start -- which is why a p2p call (or a call after - // teleport/voice-restart) could come up on the wrong device. - // - // We call workerStartDevices() rather than the full - // deployDevices() on purpose: deployDevices() also re-applies - // per-connection mute/track state, which races with the - // viewer's own mute setup for the freshly-establishing - // connection and can leave the sender track disabled (recording - // runs but nothing transmits after teleport). - workerStartDevices(); - } - - // Authoritatively (re)start capture whenever we're connected and not - // device-muted. This runs unconditionally -- NOT just in an else - // branch -- because workerStartDevices() above stops recording while - // re-selecting the device and only restarts it behind a gate; if - // that gate doesn't line up (or capture was stopped on a prior - // disconnect, e.g. teleport), this is what reliably brings the mic - // back. No-op if capture is already running. - if (!mMute && !mPeerConnections.empty() && !mDeviceModule->Recording()) - { - if (!mDeviceModule->RecordingIsInitialized()) - { - mDeviceModule->InitRecording(); - } - mDeviceModule->ForceStartRecording(); - } + workerStartPlayout(); }); } @@ -1456,12 +1420,12 @@ void LLWebRTCPeerConnectionImpl::OnConnectionChange(webrtc::PeerConnectionInterf { case webrtc::PeerConnectionInterface::PeerConnectionState::kConnected: { - // Audio is established now -- (re)start the capture and playout - // devices. Doing this here rather than at connection creation - // avoids running the output device during the handshake (heard as a - // buzz), and reliably restores the devices after a full teardown - // (teleport / voice restart). - mWebRTCImpl->startAudioDevices(); + // Audio is established now -- start playout for this connection. + // (Capture follows voice-enabled state, so it's already running and + // isn't touched here.) Doing playout here rather than at connection + // creation avoids running the output device with no decoded audio + // during the handshake (heard as a buzz). + mWebRTCImpl->startPlayout(); mPendingJobs++; webrtc::scoped_refptr self(this); mWebRTCImpl->PostWorkerTask([self]() diff --git a/indra/llwebrtc/llwebrtc.h b/indra/llwebrtc/llwebrtc.h index e76e708f0c..821400cfe8 100644 --- a/indra/llwebrtc/llwebrtc.h +++ b/indra/llwebrtc/llwebrtc.h @@ -153,6 +153,14 @@ class LLWebRTCDeviceInterface virtual void setCaptureDevice(const std::string& id) = 0; virtual void setRenderDevice(const std::string& id) = 0; + // Enable/disable the audio devices, set when voice is enabled/disabled. + // The capture (microphone) and playout (speaker) devices only run while this + // is enabled, so neither is held open when the user has voice off. While + // enabled, capture stays running across calls and mute/unmute so the AEC + // never cold-starts (no unmute hiss); playout still only runs when there's a + // connection to render. + virtual void setVoiceEnabled(bool enable) = 0; + // Device observers for device change callbacks. virtual void setDevicesObserver(LLWebRTCDevicesObserver *observer) = 0; virtual void unsetDevicesObserver(LLWebRTCDevicesObserver *observer) = 0; diff --git a/indra/llwebrtc/llwebrtc_impl.h b/indra/llwebrtc/llwebrtc_impl.h index cfb0d10c29..28d25b8d51 100644 --- a/indra/llwebrtc/llwebrtc_impl.h +++ b/indra/llwebrtc/llwebrtc_impl.h @@ -180,7 +180,7 @@ private: class LLWebRTCAudioDeviceModule : public webrtc::AudioDeviceModule { public: - explicit LLWebRTCAudioDeviceModule(webrtc::scoped_refptr inner) : inner_(std::move(inner)), tuning_(false) + explicit LLWebRTCAudioDeviceModule(webrtc::scoped_refptr inner) : inner_(inner), tuning_(false) { RTC_CHECK(inner_); } @@ -197,9 +197,15 @@ public: } int32_t Init() override { return inner_->Init(); } - int32_t Terminate() override { return inner_->Terminate(); } + int32_t Terminate() override { + // libwebrtc attempts to terminate the adm when peer connections go to zero, but we don't want that, + // now that we're keeping the adm active throughout the session. + return 0; + } bool Initialized() const override { return inner_->Initialized(); } + int32_t ForceTerminate() { return inner_->Terminate(); } + // --- Device enumeration/selection (forward) --- int16_t PlayoutDevices() override { return inner_->PlayoutDevices(); } int16_t RecordingDevices() override { return inner_->RecordingDevices(); } @@ -422,6 +428,8 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO void setCaptureDevice(const std::string& id) override; void setRenderDevice(const std::string& id) override; + void setVoiceEnabled(bool enable) override; + void setTuningMode(bool enable) override; float getTuningAudioLevel() override; float getPeerConnectionAudioLevel() override; @@ -499,16 +507,17 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO LLWebRTCPeerConnectionInterface* newPeerConnection(); void freePeerConnection(LLWebRTCPeerConnectionInterface* peer_connection); - // (Re)start the capture and playout devices once a connection's audio is - // established. This is the authoritative point for bringing the devices - // back after all connections dropped (teleport, voice restart). Idempotent - // and safe to call from any thread (work is posted to the worker thread). - void startAudioDevices(); + // Start playout once a connection's audio is established (playout is gated + // on there being a connection to render). Capture is not touched here -- + // it follows voice-enabled state, not connection state. Safe to call from + // any thread (work is posted to the worker thread). + void startPlayout(); protected: const webrtc::Environment mEnv; - void workerStartDevices(); + void workerStartRecording(); + void workerStartPlayout(); void workerDeployDevices(); // We always rely on WebRTC's internal (software APM) audio processing, so // any platform/hardware AEC/AGC/NS must be kept disabled. @@ -547,6 +556,8 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO LLWebRTCVoiceDeviceList mPlayoutDeviceList; bool mMute; + // Whether voice is enabled; gates whether the capture/playout devices run. + bool mVoiceEnabled; float mGain; LLCustomProcessorStatePtr mPeerCustomProcessor; diff --git a/indra/newview/llvoicewebrtc.cpp b/indra/newview/llvoicewebrtc.cpp index ecf963039f..126d22924b 100644 --- a/indra/newview/llvoicewebrtc.cpp +++ b/indra/newview/llvoicewebrtc.cpp @@ -1711,6 +1711,14 @@ void LLWebRTCVoiceClient::setVoiceEnabled(bool enabled) mVoiceEnabled = enabled; LLVoiceClientStatusObserver::EStatusType status; + // Gate the audio devices on voice being enabled: the capture mic and + // playout speaker only run while voice is on, and the mic isn't held + // open when voice is off. + if (mWebRTCDeviceInterface) + { + mWebRTCDeviceInterface->setVoiceEnabled(enabled); + } + if (enabled) { LL_DEBUGS("Voice") << "enabling" << LL_ENDL; -- cgit v1.3 From 71d2d15245490463f52911e08c488e228580e229 Mon Sep 17 00:00:00 2001 From: Roxie Linden Date: Mon, 20 Jul 2026 12:07:10 -0700 Subject: Use communications devices/scheme to get system audio processing --- autobuild.xml | 14 +++++++------- indra/llwebrtc/llwebrtc.cpp | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) (limited to 'indra/llwebrtc/llwebrtc.cpp') diff --git a/autobuild.xml b/autobuild.xml index 6aea4fe1fb..82a30e3c66 100644 --- a/autobuild.xml +++ b/autobuild.xml @@ -2607,11 +2607,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors archive hash - c39d041c9b68d9ae477934df9c77c53111d79f65 + 9622dbeb12d1c9592de3ea3f83c2c1a95b3c0822 hash_algorithm sha1 url - https://github.com/secondlife/3p-webrtc-build/releases/download/m144.7559.06.19/webrtc-m144.7559.06.19.29561257244-darwin64-29561257244.tar.zst + https://github.com/secondlife/3p-webrtc-build/releases/download/m144.7559.06.19.hot-mic/webrtc-m144.7559.06.19.hot-mic.29761579711-darwin64-29761579711.tar.zst name darwin64 @@ -2621,11 +2621,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors archive hash - 404775fa5ecd1d6eedbc34916df07cb70b8bbfdb + 9113fdfafca5ca750e415abafaf080540346e068 hash_algorithm sha1 url - https://github.com/secondlife/3p-webrtc-build/releases/download/m144.7559.06.19/webrtc-m144.7559.06.19.29561257244-linux64-29561257244.tar.zst + https://github.com/secondlife/3p-webrtc-build/releases/download/m144.7559.06.19.hot-mic/webrtc-m144.7559.06.19.hot-mic.29761579711-linux64-29761579711.tar.zst name linux64 @@ -2635,11 +2635,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors archive hash - 28a8d62165fd9cdf0de921879b88b098c0cb7076 + 1d8a82b72887abfb1c5014fcca4337703dfdcd25 hash_algorithm sha1 url - https://github.com/secondlife/3p-webrtc-build/releases/download/m144.7559.06.19/webrtc-m144.7559.06.19.29561257244-windows64-29561257244.tar.zst + https://github.com/secondlife/3p-webrtc-build/releases/download/m144.7559.06.19.hot-mic/webrtc-m144.7559.06.19.hot-mic.29761579711-windows64-29761579711.tar.zst name windows64 @@ -2652,7 +2652,7 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors copyright Copyright (c) 2011, The WebRTC project authors. All rights reserved. version - m144.7559.06.19.29561257244 + m144.7559.06.19.hot-mic.29761579711 name webrtc vcs_branch diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp index 6c809f2743..3d58e4e1ce 100644 --- a/indra/llwebrtc/llwebrtc.cpp +++ b/indra/llwebrtc/llwebrtc.cpp @@ -42,8 +42,8 @@ namespace llwebrtc { #if WEBRTC_WIN -static int16_t PLAYOUT_DEVICE_DEFAULT = webrtc::AudioDeviceModule::kDefaultDevice; -static int16_t RECORD_DEVICE_DEFAULT = webrtc::AudioDeviceModule::kDefaultDevice; +static int16_t PLAYOUT_DEVICE_DEFAULT = webrtc::AudioDeviceModule::kDefaultCommunicationDevice; +static int16_t RECORD_DEVICE_DEFAULT = webrtc::AudioDeviceModule::kDefaultCommunicationDevice; #else static int16_t PLAYOUT_DEVICE_DEFAULT = 0; static int16_t RECORD_DEVICE_DEFAULT = 0; -- cgit v1.3 From cef8c85e2c70adbc42d9c2e4894633ad657f355d Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:39:54 +0300 Subject: p#682 Improve logging for system events and session (#6086) --- indra/llcommon/llwatchdog.cpp | 5 +++ indra/llcommon/llwatchdog.h | 2 + indra/llui/llfloater.cpp | 2 + indra/llwebrtc/llwebrtc.cpp | 4 ++ indra/llwindow/llwindowcallbacks.cpp | 2 +- indra/llwindow/llwindowcallbacks.h | 2 +- indra/llwindow/llwindowwin32.cpp | 71 ++++++++++++++++++++++++++++++++++-- indra/newview/llappviewer.cpp | 15 ++++++-- indra/newview/llviewerwindow.cpp | 7 +++- indra/newview/llviewerwindow.h | 2 +- 10 files changed, 100 insertions(+), 12 deletions(-) (limited to 'indra/llwebrtc/llwebrtc.cpp') diff --git a/indra/llcommon/llwatchdog.cpp b/indra/llcommon/llwatchdog.cpp index 66b565c763..886f19366c 100644 --- a/indra/llcommon/llwatchdog.cpp +++ b/indra/llcommon/llwatchdog.cpp @@ -116,6 +116,11 @@ bool LLWatchdogTimeout::isAlive() const return (mTimer.getStarted() && !mTimer.hasExpired()); } +bool LLWatchdogTimeout::started() const +{ + return mTimer.getStarted(); +} + void LLWatchdogTimeout::reset() { mTimer.setTimerExpirySec(mTimeout); diff --git a/indra/llcommon/llwatchdog.h b/indra/llcommon/llwatchdog.h index f138fbccb0..d55bf434f3 100644 --- a/indra/llcommon/llwatchdog.h +++ b/indra/llcommon/llwatchdog.h @@ -47,6 +47,7 @@ public: // This may mean that resources used by // isAlive and other method may need synchronization. virtual bool isAlive() const = 0; + virtual bool started() const = 0; virtual void reset() = 0; virtual void start(); virtual void stop(); @@ -66,6 +67,7 @@ public: virtual ~LLWatchdogTimeout(); bool isAlive() const override; + bool started() const override; void reset() override; void start() override { start(""); } void stop() override; diff --git a/indra/llui/llfloater.cpp b/indra/llui/llfloater.cpp index 9361358ced..6de2c18620 100644 --- a/indra/llui/llfloater.cpp +++ b/indra/llui/llfloater.cpp @@ -569,6 +569,8 @@ void LLFloater::storeRectControl() void LLFloater::storeVisibilityControl() { + // Todo: this is a bit pricey, gets called each frame + // on LLAppViewer::idle(), optimize! if( !sQuitting && mVisibilityControl.size() > 1 ) { getControlGroup()->setBOOL( mVisibilityControl, getVisible() ); diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp index 3d58e4e1ce..80f2c46332 100644 --- a/indra/llwebrtc/llwebrtc.cpp +++ b/indra/llwebrtc/llwebrtc.cpp @@ -748,6 +748,7 @@ void LLWebRTCImpl::updateDevices() char name[webrtc::kAdmMaxDeviceNameSize]; char guid[webrtc::kAdmMaxGuidSize]; mDeviceModule->PlayoutDeviceName(index, name, guid); + RTC_LOG(LS_VERBOSE) << "updateDevices: playout device [" << index << "] name='" << name << "' guid='" << guid << "'"; mPlayoutDeviceList.emplace_back(name, guid); } @@ -766,9 +767,12 @@ void LLWebRTCImpl::updateDevices() char name[webrtc::kAdmMaxDeviceNameSize]; char guid[webrtc::kAdmMaxGuidSize]; mDeviceModule->RecordingDeviceName(index, name, guid); + RTC_LOG(LS_VERBOSE) << "updateDevices: recording device [" << index << "] name='" << name << "' guid='" << guid << "'"; mRecordingDeviceList.emplace_back(name, guid); } + RTC_LOG(LS_INFO) << "updateDevices, playout count: " << renderDeviceCount << "; capture count: " << captureDeviceCount; + for (auto &observer : mVoiceDevicesObserverList) { observer->OnDevicesChanged(mPlayoutDeviceList, mRecordingDeviceList); diff --git a/indra/llwindow/llwindowcallbacks.cpp b/indra/llwindow/llwindowcallbacks.cpp index 7331f50ba0..4b804c82cc 100644 --- a/indra/llwindow/llwindowcallbacks.cpp +++ b/indra/llwindow/llwindowcallbacks.cpp @@ -190,7 +190,7 @@ bool LLWindowCallbacks::handleTimerEvent(LLWindow *window) return false; } -bool LLWindowCallbacks::handleDeviceChange(LLWindow *window) +bool LLWindowCallbacks::handleDeviceChange(LLWindow *window, const std::string& change_type) { return false; } diff --git a/indra/llwindow/llwindowcallbacks.h b/indra/llwindow/llwindowcallbacks.h index 59dcdd3ade..6d1990e92b 100644 --- a/indra/llwindow/llwindowcallbacks.h +++ b/indra/llwindow/llwindowcallbacks.h @@ -68,7 +68,7 @@ public: virtual void handleWindowUnblock(LLWindow *window); // window coming back after taking over CPU for a while virtual void handleDataCopy(LLWindow *window, S32 data_type, void *data); virtual bool handleTimerEvent(LLWindow *window); - virtual bool handleDeviceChange(LLWindow *window); + virtual bool handleDeviceChange(LLWindow *window, const std::string& change_type); virtual bool handleDPIChanged(LLWindow *window, F32 ui_scale_factor, S32 window_width, S32 window_height); virtual bool handleDisplayChanged(); virtual bool handleWindowDidChangeScreen(LLWindow *window); diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index 6230cc3026..562dfc55ab 100644 --- a/indra/llwindow/llwindowwin32.cpp +++ b/indra/llwindow/llwindowwin32.cpp @@ -452,6 +452,16 @@ struct LLWindowWin32::LLWindowWin32Thread : public LL::ThreadPool } }); } + + // For mainWindowProc, it should not unpause watchdog if it was paused + void pingWindowTimeout(std::string_view state) + { + if (mWindowTimeout && mWindowTimeout->started()) + { + mWindowTimeout->setTimeout(WINDOW_TIMEOUT_SEC); + mWindowTimeout->ping(state); + } + } private: // These timeout related functions are strictly for the thread. void resumeTimeout(std::string_view state) @@ -2413,18 +2423,45 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ case WM_DEVICECHANGE: { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_DEVICECHANGE"); + window_imp->mWindowThread->pingWindowTimeout("WM_DEVICECHANGE"); + + // Log detailed device change information + std::string change_type = "UNKNOWN"; + switch (w_param) + { + case DBT_DEVICEARRIVAL: change_type = "DBT_DEVICEARRIVAL"; break; + case DBT_DEVICEREMOVECOMPLETE: change_type = "DBT_DEVICEREMOVECOMPLETE"; break; + case DBT_DEVNODES_CHANGED: change_type = "DBT_DEVNODES_CHANGED"; break; + case DBT_DEVICEQUERYREMOVE: change_type = "DBT_DEVICEQUERYREMOVE"; break; + case DBT_DEVICEQUERYREMOVEFAILED: change_type = "DBT_DEVICEQUERYREMOVEFAILED"; break; + case DBT_DEVICEREMOVEPENDING: change_type = "DBT_DEVICEREMOVEPENDING"; break; + case DBT_CONFIGCHANGED: change_type = "DBT_CONFIGCHANGED"; break; + } + if (w_param == DBT_DEVNODES_CHANGED || w_param == DBT_DEVICEARRIVAL) { - WINDOW_IMP_POST(window_imp->mCallbacks->handleDeviceChange(window_imp)); + WINDOW_IMP_POST(window_imp->mCallbacks->handleDeviceChange(window_imp, change_type)); return 1; } + else if (l_param) + { + const auto* hdr = reinterpret_cast(l_param); + if (hdr->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) + { + // Might need to register for monitor device notifications + // to get this message when monitor is suspended or resumed. + // TODO: log monitor suspending and resuming. + LL_INFOS("Window") << "DEVICEINTERFACE: " << change_type << LL_ENDL; + } + } break; } case WM_PAINT: { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_PAINT"); + window_imp->mWindowThread->pingWindowTimeout("WM_PAINT"); GetUpdateRect(window_imp->mWindowHandle, &update_rect, FALSE); update_width = update_rect.right - update_rect.left + 1; update_height = update_rect.bottom - update_rect.top + 1; @@ -2467,6 +2504,15 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ break; } + case WM_POWERBROADCAST: + { + // Might need to register for power broadcast interface + // Todo: log monitor suspending and resuming. + LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_POWERBROADCAST"); + LL_INFOS("Window") << "Received WM_POWERBROADCAST with wParam: 0x" << std::hex << (uintptr_t)w_param << " lParam: 0x" << (uintptr_t)l_param << std::dec << LL_ENDL; + break; + } + case WM_ACTIVATEAPP: { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_ACTIVATEAPP"); @@ -2540,6 +2586,7 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ case WM_CLOSE: { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_CLOSE"); + window_imp->mWindowThread->pingWindowTimeout("WM_CLOSE"); // todo: WM_CLOSE can be caused by user and by task manager, // distinguish these cases. // For now assume it is always user. @@ -2577,6 +2624,7 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ // Comes after WM_QUERYENDSESSION LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_ENDSESSION"); LL_INFOS("Window") << "Received WM_ENDSESSION with wParam: " << (U32)w_param << " lParam: " << (U32)l_param << LL_ENDL; + window_imp->mWindowThread->pingWindowTimeout("WM_ENDSESSION"); unsigned int end_session_flags = (U32)l_param; if (w_param == TRUE // if true, session is ending @@ -3112,6 +3160,7 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ case WM_DPICHANGED: { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_DPICHANGED"); + window_imp->mWindowThread->pingWindowTimeout("WM_DPICHANGED"); LPRECT lprc_new_scale; F32 new_scale = F32(LOWORD(w_param)) / F32(USER_DEFAULT_SCREEN_DPI); lprc_new_scale = (LPRECT)l_param; @@ -3132,7 +3181,9 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ case WM_DISPLAYCHANGE: { + window_imp->mWindowThread->pingWindowTimeout("WM_DISPLAYCHANGE"); WINDOW_IMP_POST(window_imp->mCallbacks->handleDisplayChanged()); + break; } case WM_SETFOCUS: @@ -3170,6 +3221,9 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ case WM_SETTINGCHANGE: { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_SETTINGCHANGE"); + // Can be called on OS user switching + LL_INFOS("Window") << "WM_SETTINGCHANGE, with wParam: 0x" << std::hex << (uintptr_t)w_param << " lParam: 0x" << (uintptr_t)l_param << std::dec << LL_ENDL; + window_imp->mWindowThread->pingWindowTimeout("WM_SETTINGCHANGE"); if (w_param == SPI_SETMOUSEVANISH) { if (!SystemParametersInfo(SPI_GETMOUSEVANISH, 0, &window_imp->mMouseVanish, 0)) @@ -5014,6 +5068,13 @@ inline LLWindowWin32::LLWindowWin32Thread::LLWindowWin32Thread() : LL::ThreadPool("Window Thread", 1, MAX_QUEUE_SIZE, false) { LL::ThreadPool::start(); + + // Set thread name for the window thread + // This will make it distinguishable in Visual Studio debugger + post([this]() + { + SetThreadDescription(GetCurrentThread(), L"LLWindowWin32 Thread"); + }); } /** @@ -5195,7 +5256,7 @@ void LLWindowWin32::LLWindowWin32Thread::run() } // Normally won't exist yet, but in case of re-init, make sure it's cleaned up - resumeTimeout("WindowThread"); + resumeTimeout("Window:WindowThread"); while (! getQueue().done()) { @@ -5206,23 +5267,25 @@ void LLWindowWin32::LLWindowWin32Thread::run() if (mWindowHandleThrd != 0) { - pingTimeout("messages"); MSG msg; BOOL status; if (mhDCThrd == 0) { + pingTimeout("Window:PeekMessage"); LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("w32t - PeekMessage"); logger.onChange("PeekMessage(", std::hex, mWindowHandleThrd, ")"); status = PeekMessage(&msg, mWindowHandleThrd, 0, 0, PM_REMOVE); } else { + pingTimeout("Window:GetMessage"); LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("w32t - GetMessage"); logger.always("GetMessage(", std::hex, mWindowHandleThrd, ")"); status = GetMessage(&msg, NULL, 0, 0); } if (status > 0) { + pingTimeout("Window:TranslateMessage"); logger.always("got MSG (", std::hex, msg.hwnd, ", ", msg.message, ", ", msg.wParam, ")"); TranslateMessage(&msg); @@ -5234,7 +5297,7 @@ void LLWindowWin32::LLWindowWin32Thread::run() { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("w32t - Function Queue"); - pingTimeout("queue"); + pingTimeout("Window:Queue"); logger.onChange("runPending()"); //process any pending functions getQueue().runPending(); diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 0b46e2ccc3..0a2b8a7d3d 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -2989,13 +2989,20 @@ bool LLAppViewer::initConfiguration() if (mSecondInstance) { - // This is the second instance of SL. Mute voice, - // but make sure the setting is *not* persisted. + // This is the second concurrent instance of SL. + // Disable voice for this session only, user should + // be able to enable voice manually, after that it + // works the same way as on primary instance. LLControlVariable* enable_voice = gSavedSettings.getControl("EnableVoiceChat"); - if (enable_voice) + if (enable_voice && enable_voice->getValue().asBoolean()) { + LL_DEBUGS("AppInit") << "Disabling voice for this session only" << LL_ENDL; + // Will be saved as mValues[2] which does not get written to the file. + // This feels like a hack, but otherwise way too many controls have to + // be tracked manually instead of using xmls' control_name. const bool DO_NOT_PERSIST = false; - enable_voice->setValue(LLSD(false), DO_NOT_PERSIST); + LLSD::Boolean new_value = false; + enable_voice->setValue(new_value, DO_NOT_PERSIST); } } diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp index dea96e2012..b06c129974 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -1777,7 +1777,7 @@ bool LLViewerWindow::handleTimerEvent(LLWindow *window) return false; } -bool LLViewerWindow::handleDeviceChange(LLWindow *window) +bool LLViewerWindow::handleDeviceChange(LLWindow *window, const std::string& change_type) { // give a chance to use a joystick after startup (hot-plugging) if (!LLViewerJoystick::getInstance()->isJoystickInitialized() ) @@ -1785,6 +1785,10 @@ bool LLViewerWindow::handleDeviceChange(LLWindow *window) LLViewerJoystick::getInstance()->init(true); return true; } + else + { + LL_INFOS("Window") << "Device change event: " << change_type << LL_ENDL; + } return false; } @@ -1806,6 +1810,7 @@ bool LLViewerWindow::handleDPIChanged(LLWindow *window, F32 ui_scale_factor, S32 bool LLViewerWindow::handleDisplayChanged() { + LL_INFOS("Window") << "Display change event" << LL_ENDL; LLFontGL::sResolutionGeneration++; return false; } diff --git a/indra/newview/llviewerwindow.h b/indra/newview/llviewerwindow.h index 5f1afe2cbe..c748f051dd 100644 --- a/indra/newview/llviewerwindow.h +++ b/indra/newview/llviewerwindow.h @@ -231,7 +231,7 @@ public: /*virtual*/ void handleWindowUnblock(LLWindow *window); /*virtual*/ void handleDataCopy(LLWindow *window, S32 data_type, void *data); /*virtual*/ bool handleTimerEvent(LLWindow *window); - /*virtual*/ bool handleDeviceChange(LLWindow *window); + /*virtual*/ bool handleDeviceChange(LLWindow *window, const std::string& change_type); /*virtual*/ bool handleDPIChanged(LLWindow *window, F32 ui_scale_factor, S32 window_width, S32 window_height); /*virtual*/ bool handleDisplayChanged(); /*virtual*/ bool handleWindowDidChangeScreen(LLWindow *window); -- cgit v1.3 From 35e7b998622fecb75d4dc01771f02d2d60443637 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Mon, 3 Aug 2026 22:52:13 +0300 Subject: p#682 WebRTC can lock up after OS sleep --- indra/llwebrtc/llwebrtc.cpp | 90 +++++++++++++++++++++++++++++++++--------- indra/llwebrtc/llwebrtc_impl.h | 3 ++ 2 files changed, 75 insertions(+), 18 deletions(-) (limited to 'indra/llwebrtc/llwebrtc.cpp') diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp index 80f2c46332..abb22ac237 100644 --- a/indra/llwebrtc/llwebrtc.cpp +++ b/indra/llwebrtc/llwebrtc.cpp @@ -26,6 +26,9 @@ #include "llwebrtc_impl.h" #include +#include +#include +#include #include #include "api/audio/create_audio_device_module.h" #include "api/audio_codecs/audio_decoder_factory.h" @@ -395,8 +398,17 @@ void LLWebRTCImpl::init() void LLWebRTCImpl::terminate() { - mWorkerThread->BlockingCall( - [this]() + // Run all blocking WebRTC shutdown calls on a separate thread so that a + // hung BlockingCall cannot block the viewer shutdown indefinitely. + // Webrtc is not mission critical, we need to save personal data. + auto done_promise = std::make_shared >(); + std::future done_future = done_promise->get_future(); + + std::thread shutdown_thread( + [this, done_promise]() mutable + { + mWorkerThread->BlockingCall( + [this]() { if (mDeviceModule) { @@ -405,27 +417,52 @@ void LLWebRTCImpl::terminate() } }); - for (auto &connection : mPeerConnections) - { - connection->terminate(); - } - - // connection->terminate() above spawns a number of Signaling thread calls to - // shut down the connection. The following Blocking Call will wait - // until they're done before it's executed, allowing time to clean up. + for (auto& connection : mPeerConnections) + { + connection->terminate(); + } - mSignalingThread->BlockingCall([this]() { mPeerConnectionFactory = nullptr; }); + // connection->terminate() above spawns a number of Signaling thread calls to + // shut down the connection. The following Blocking Call will wait + // until they're done before it's executed, allowing time to clean up. + mSignalingThread->BlockingCall([this]() { mPeerConnectionFactory = nullptr; }); - mWorkerThread->BlockingCall( - [this]() + mWorkerThread->BlockingCall( + [this]() { if (mDeviceModule) { mDeviceModule->ForceTerminate(); } - mDeviceModule = nullptr; + mDeviceModule = nullptr; }); + done_promise->set_value(); + }); + + constexpr auto WEBRTC_TERMINATE_TIMEOUT = std::chrono::seconds(10); + if (done_future.wait_for(WEBRTC_TERMINATE_TIMEOUT) == std::future_status::timeout) + { + RTC_LOG(LS_WARNING) << __FUNCTION__ + << ": timed out waiting for WebRTC thread shutdown." + " 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(); + + mPeerConnections.clear(); + webrtc::LogMessage::RemoveLogToStream(mLogSink); + return; + } + + shutdown_thread.join(); + // In case peer connections still somehow have jobs in workers, // only clear connections up after clearing workers. mNetworkThread = nullptr; @@ -982,6 +1019,7 @@ LLWebRTCPeerConnectionImpl::LLWebRTCPeerConnectionImpl(const webrtc::Environment mAnswerReceived(false), mPeerConnectionState(webrtc::PeerConnectionInterface::PeerConnectionState::kNew), mDisconnectCount(0), + mStatsRequestPending(false), mPendingJobs(0) { } @@ -1781,16 +1819,32 @@ void LLWebRTCPeerConnectionImpl::gatherConnectionStats() return; } - auto stats_callback = webrtc::make_ref_counted( - [this](const LLWebRTCStatsMap& generic_stats) + webrtc::scoped_refptr self(this); + mWebRTCImpl->PostSignalingTask( + [self]() + { + if (!self->mPeerConnection + || self->mPeerConnectionState != webrtc::PeerConnectionInterface::PeerConnectionState::kConnected + || self->mStatsRequestPending) // signaling thread only { - for (auto& observer : mSignalingObserverList) + return; + } + + self->mStatsRequestPending = true; + + auto stats_callback = webrtc::make_ref_counted( + [self](const LLWebRTCStatsMap& generic_stats) + { + self->mStatsRequestPending = false; + + for (auto& observer : self->mSignalingObserverList) { observer->OnStatsDelivered(generic_stats); } }); - mPeerConnection->GetStats(stats_callback.get()); + self->mPeerConnection->GetStats(stats_callback.get()); + }); } LLWebRTCImpl * gWebRTCImpl = nullptr; diff --git a/indra/llwebrtc/llwebrtc_impl.h b/indra/llwebrtc/llwebrtc_impl.h index 28d25b8d51..551d4a3fd9 100644 --- a/indra/llwebrtc/llwebrtc_impl.h +++ b/indra/llwebrtc/llwebrtc_impl.h @@ -688,6 +688,9 @@ class LLWebRTCPeerConnectionImpl : public LLWebRTCPeerConnectionInterface, webrtc::PeerConnectionInterface::PeerConnectionState mPeerConnectionState; uint32_t mDisconnectCount; + // Accessed only on the WebRTC signaling thread. + bool mStatsRequestPending; + std::atomic mPendingJobs; }; -- cgit v1.3 From 0686467f42a06ec84368851f48f31beb43888df0 Mon Sep 17 00:00:00 2001 From: Roxie Linden Date: Wed, 5 Aug 2026 16:10:40 -0700 Subject: Terminate open connections on the signaling thread during shutdown. --- indra/llwebrtc/llwebrtc.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) (limited to 'indra/llwebrtc/llwebrtc.cpp') diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp index abb22ac237..8b04cae6ad 100644 --- a/indra/llwebrtc/llwebrtc.cpp +++ b/indra/llwebrtc/llwebrtc.cpp @@ -417,15 +417,21 @@ void LLWebRTCImpl::terminate() } }); - for (auto& connection : mPeerConnections) - { - connection->terminate(); - } + mSignalingThread->PostTask( + [this]() + { + for (auto& connection : mPeerConnections) + { + connection->terminate(); + } + }); - // connection->terminate() above spawns a number of Signaling thread calls to + // connection->terminate() above spawns a number of additional Signaling thread calls to // shut down the connection. The following Blocking Call will wait // until they're done before it's executed, allowing time to clean up. - mSignalingThread->BlockingCall([this]() { mPeerConnectionFactory = nullptr; }); + mSignalingThread->BlockingCall([this]() { + mPeerConnectionFactory = nullptr; + }); mWorkerThread->BlockingCall( [this]() -- cgit v1.3 From 8c5ec43ebc5c51372e2ebf4e3cb13faa7f1283e7 Mon Sep 17 00:00:00 2001 From: Roxie Linden Date: Thu, 6 Aug 2026 16:54:44 -0700 Subject: p#682 Fix shutdown crash flushing peer connection stats LLWebRTCImpl::terminate() only posted the peer connection closes to the signaling thread, so they ran behind the BlockingCall that releases the factory -- after the factory and device module were gone, or not at all if the thread was destroyed with the task still queued. That matters for stats: PeerConnection::Close() flushes any in-flight GetStats request and delivers the report inline, and our callback walks mSignalingObserverList to hand it to the viewer. By then the viewer's LLVoiceWebRTCConnection objects have been destroyed -- on shutdown they deliberately skip unsetSignalingObserver, on the assumption that llwebrtc::terminate() already finished the job -- so the stats callback reaches into freed memory. Split the close out of LLWebRTCPeerConnectionImpl::terminate() into closeOnSignalingThread() and run it from a BlockingCall, so connections are closed and destroyed on the signaling thread before the factory, the device module or the observers go away. Also take ownership of the connection list up front so the detached thread in the timeout path isn't racing the main thread over it, clear the observer lists as part of the close, and skip the observer notification for a stats report delivered while shutting down. Co-Authored-By: Claude Opus 5 (1M context) --- indra/llwebrtc/llwebrtc.cpp | 145 +++++++++++++++++++++++++++-------------- indra/llwebrtc/llwebrtc_impl.h | 10 +++ 2 files changed, 106 insertions(+), 49 deletions(-) (limited to 'indra/llwebrtc/llwebrtc.cpp') diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp index 8b04cae6ad..bdb629172d 100644 --- a/indra/llwebrtc/llwebrtc.cpp +++ b/indra/llwebrtc/llwebrtc.cpp @@ -404,8 +404,14 @@ void LLWebRTCImpl::terminate() auto done_promise = std::make_shared >(); std::future done_future = done_promise->get_future(); + // Hand ownership of the connections to the shutdown thread. Nothing on + // this thread may touch them afterwards -- in the timeout case below the + // shutdown thread is detached and may still be working through them. + std::vector> connections; + connections.swap(mPeerConnections); + std::thread shutdown_thread( - [this, done_promise]() mutable + [this, connections = std::move(connections), done_promise]() mutable { mWorkerThread->BlockingCall( [this]() @@ -417,18 +423,32 @@ void LLWebRTCImpl::terminate() } }); - mSignalingThread->PostTask( - [this]() + // Close the connections inline on the signaling thread. This can't be + // connection->terminate(), which only *posts* the close: that queues the + // real work behind everything below, so the connections would be closed + // after the factory and the device module are gone -- or not at all, if + // the thread is destroyed with the task still queued. + // + // It matters that the close completes here because closing a peer + // connection flushes any in-flight GetStats request and runs its + // callback inline, and that callback calls back into the viewer's + // signaling observers. Those observers are only valid until + // llwebrtc::terminate() returns. + mSignalingThread->BlockingCall( + [&connections]() { - for (auto& connection : mPeerConnections) + for (auto& connection : connections) { - connection->terminate(); + connection->closeOnSignalingThread(); } + // Destroy the connections here, on the signaling thread, while + // it's still running. + connections.clear(); }); - // connection->terminate() above spawns a number of additional Signaling thread calls to - // shut down the connection. The following Blocking Call will wait - // until they're done before it's executed, allowing time to clean up. + // Drain anything the closes posted before dropping the factory. + mSignalingThread->BlockingCall([]() {}); + mSignalingThread->BlockingCall([this]() { mPeerConnectionFactory = nullptr; }); @@ -462,20 +482,20 @@ void LLWebRTCImpl::terminate() (void)mWorkerThread.release(); (void)mSignalingThread.release(); - mPeerConnections.clear(); + // mPeerConnections is already empty -- the detached thread owns the + // connections now and must be left to finish with them. webrtc::LogMessage::RemoveLogToStream(mLogSink); return; } shutdown_thread.join(); - // In case peer connections still somehow have jobs in workers, - // only clear connections up after clearing workers. + // 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; - mPeerConnections.clear(); webrtc::LogMessage::RemoveLogToStream(mLogSink); } @@ -1026,6 +1046,7 @@ LLWebRTCPeerConnectionImpl::LLWebRTCPeerConnectionImpl(const webrtc::Environment mPeerConnectionState(webrtc::PeerConnectionInterface::PeerConnectionState::kNew), mDisconnectCount(0), mStatsRequestPending(false), + mShuttingDown(false), mPendingJobs(0) { } @@ -1058,47 +1079,64 @@ void LLWebRTCPeerConnectionImpl::terminate() mWebRTCImpl->PostSignalingTask( [self]() { - if (self->mPeerConnection) - { - if (self->mDataChannel) - { - { - self->mDataChannel->Close(); - self->mDataChannel = nullptr; - } - } + self->closeOnSignalingThread(); + self->mPendingJobs--; + }); +} - // to remove 'Secondlife is recording' icon from taskbar - // if user was speaking - auto senders = self->mPeerConnection->GetSenders(); - for (auto& sender : senders) - { - auto track = sender->track(); - if (track) - { - track->set_enabled(false); - } - } +// Signaling thread only. +void LLWebRTCPeerConnectionImpl::closeOnSignalingThread() +{ + // Stop issuing stats requests; one may already be in flight, and + // Close() below will flush it. + mShuttingDown = true; - self->mPeerConnection->Close(); - if (self->mLocalStream) - { - auto tracks = self->mLocalStream->GetAudioTracks(); - for (auto& track : tracks) - { - self->mLocalStream->RemoveTrack(track); - } - self->mLocalStream = nullptr; - } - self->mPeerConnection = nullptr; + if (mPeerConnection) + { + if (mDataChannel) + { + mDataChannel->Close(); + mDataChannel = nullptr; + } - for (auto &observer : self->mSignalingObserverList) - { - observer->OnPeerConnectionClosed(); - } + // to remove 'Secondlife is recording' icon from taskbar + // if user was speaking + auto senders = mPeerConnection->GetSenders(); + for (auto& sender : senders) + { + auto track = sender->track(); + if (track) + { + track->set_enabled(false); } - self->mPendingJobs--; - }); + } + + // NOTE: Close() delivers any pending GetStats report inline, before it + // returns, so the observer list below must still be valid here. + mPeerConnection->Close(); + if (mLocalStream) + { + auto tracks = mLocalStream->GetAudioTracks(); + for (auto& track : tracks) + { + mLocalStream->RemoveTrack(track); + } + mLocalStream = nullptr; + } + mPeerConnection = nullptr; + + 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. + mSignalingObserverList.clear(); + mDataObserverList.clear(); } void LLWebRTCPeerConnectionImpl::setSignalingObserver(LLWebRTCSignalingObserver *observer) { mSignalingObserverList.emplace_back(observer); } @@ -1830,6 +1868,7 @@ void LLWebRTCPeerConnectionImpl::gatherConnectionStats() [self]() { if (!self->mPeerConnection + || self->mShuttingDown || self->mPeerConnectionState != webrtc::PeerConnectionInterface::PeerConnectionState::kConnected || self->mStatsRequestPending) // signaling thread only { @@ -1843,6 +1882,14 @@ void LLWebRTCPeerConnectionImpl::gatherConnectionStats() { self->mStatsRequestPending = false; + // This can be delivered inline from PeerConnection::Close(), which + // flushes pending stats requests as it tears down. Don't call out + // to the observers in that case -- we're on our way out. + if (!self->mPeerConnection || self->mShuttingDown) + { + return; + } + for (auto& observer : self->mSignalingObserverList) { observer->OnStatsDelivered(generic_stats); diff --git a/indra/llwebrtc/llwebrtc_impl.h b/indra/llwebrtc/llwebrtc_impl.h index 551d4a3fd9..cee42cf19a 100644 --- a/indra/llwebrtc/llwebrtc_impl.h +++ b/indra/llwebrtc/llwebrtc_impl.h @@ -585,7 +585,12 @@ class LLWebRTCPeerConnectionImpl : public LLWebRTCPeerConnectionInterface, ~LLWebRTCPeerConnectionImpl(); void init(LLWebRTCImpl * webrtc_impl); + // Posts closeOnSignalingThread() and returns immediately. void terminate(); + // The actual close. Signaling thread only. Callable directly (via a + // BlockingCall) when the caller needs the connection to be fully closed + // before it continues -- see LLWebRTCImpl::terminate(). + void closeOnSignalingThread(); virtual void AddRef() const override = 0; virtual webrtc::RefCountReleaseStatus Release() const override = 0; @@ -691,6 +696,11 @@ class LLWebRTCPeerConnectionImpl : public LLWebRTCPeerConnectionInterface, // Accessed only on the WebRTC signaling thread. bool mStatsRequestPending; + // Set by closeOnSignalingThread() so that no new stats request (or other + // callback into the viewer) is issued while we're tearing down. + // Accessed only on the WebRTC signaling thread. + bool mShuttingDown; + std::atomic mPendingJobs; }; -- cgit v1.3