summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorErik Kundiman <erik@megapahit.org>2026-10-22 11:41:46 +0800
committerErik Kundiman <erik@megapahit.org>2026-10-22 11:41:46 +0800
commit80e7d54b419b591a0dbb2a86a413e29f87c7e17e (patch)
treeebf7fbcd3ef2ab3c5d2cd189a31dc6c5b8082dd6
parentdc3524d24ae63c1024c094b67fb13f41f49d5637 (diff)
parentd567465ea17a2523b200fbf6213adaacfccd23da (diff)
Merge tag 'Second_Life_Release#d567465e-26.5' into 26.5
-rw-r--r--autobuild.xml24
-rw-r--r--indra/llfilesystem/lldir.cpp2
-rw-r--r--indra/llfilesystem/lldir_mac.cpp7
-rw-r--r--indra/llwebrtc/CMakeLists.txt4
-rw-r--r--indra/llwebrtc/llwebrtc.cpp175
-rw-r--r--indra/llwebrtc/llwebrtc.h10
-rw-r--r--indra/llwebrtc/llwebrtc_impl.h22
-rw-r--r--indra/newview/app_settings/settings.xml11
-rw-r--r--indra/newview/llagentcamera.cpp4
-rw-r--r--indra/newview/llchatservicehistory.cpp168
-rw-r--r--indra/newview/llvoicewebrtc.cpp31
-rw-r--r--indra/newview/llvoicewebrtc.h1
-rw-r--r--indra/newview/skins/default/xui/en/panel_preferences_sound.xml41
13 files changed, 386 insertions, 114 deletions
diff --git a/autobuild.xml b/autobuild.xml
index 95cf33ae12..4315da94fd 100644
--- a/autobuild.xml
+++ b/autobuild.xml
@@ -2462,11 +2462,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
<key>archive</key>
<map>
<key>hash</key>
- <string>9622dbeb12d1c9592de3ea3f83c2c1a95b3c0822</string>
+ <string>516b69b841109d38bd14286d28a8d77ee67e1c52</string>
<key>hash_algorithm</key>
<string>sha1</string>
<key>url</key>
- <string>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</string>
+ <string>https://github.com/secondlife/3p-webrtc-build/releases/download/m144.7559.06.22/webrtc-m144.7559.06.22.38629291819-darwin64-38629291819.tar.zst</string>
</map>
<key>name</key>
<string>darwin64</string>
@@ -2476,11 +2476,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
<key>archive</key>
<map>
<key>hash</key>
- <string>9113fdfafca5ca750e415abafaf080540346e068</string>
+ <string>69429645f9f5eb0080546af0f4e32082f285a852</string>
<key>hash_algorithm</key>
<string>sha1</string>
<key>url</key>
- <string>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</string>
+ <string>https://github.com/secondlife/3p-webrtc-build/releases/download/m144.7559.06.22/webrtc-m144.7559.06.22.38629291819-linux64-38629291819.tar.zst</string>
</map>
<key>name</key>
<string>linux64</string>
@@ -2490,11 +2490,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
<key>archive</key>
<map>
<key>hash</key>
- <string>1d8a82b72887abfb1c5014fcca4337703dfdcd25</string>
+ <string>a9ef8bafede7a26d561faca7d770a31aebeda3a5</string>
<key>hash_algorithm</key>
<string>sha1</string>
<key>url</key>
- <string>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</string>
+ <string>https://github.com/secondlife/3p-webrtc-build/releases/download/m144.7559.06.22/webrtc-m144.7559.06.22.38629291819-windows64-38629291819.tar.zst</string>
</map>
<key>name</key>
<string>windows64</string>
@@ -2507,7 +2507,7 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
<key>copyright</key>
<string>Copyright (c) 2011, The WebRTC project authors. All rights reserved.</string>
<key>version</key>
- <string>m144.7559.06.19.hot-mic.29761579711</string>
+ <string>m144.7559.06.22.38629291819</string>
<key>name</key>
<string>webrtc</string>
<key>vcs_branch</key>
@@ -2752,11 +2752,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
<key>archive</key>
<map>
<key>hash</key>
- <string>df2260187110aa51c20101183e18a55f86e71090</string>
+ <string>6649ba5678c8823c1eb03e0e0c99bebbf1691921</string>
<key>hash_algorithm</key>
<string>sha1</string>
<key>url</key>
- <string>https://github.com/secondlife-3p/3p-velopack/releases/download/v0.0.1535-r2/velopack-40232ef.24685318450-windows64-24685318450.tar.zst</string>
+ <string>https://github.com/secondlife-3p/3p-velopack/releases/download/v1.2.161/velopack-92d6a1c.39500165822-windows64-39500165822.tar.zst</string>
</map>
<key>name</key>
<string>windows64</string>
@@ -2766,11 +2766,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
<key>archive</key>
<map>
<key>hash</key>
- <string>ead94386d7b9a143824d7ccedc86433127028a91</string>
+ <string>2fa21fefd35c69908013eb41a9f86f07e136d19a</string>
<key>hash_algorithm</key>
<string>sha1</string>
<key>url</key>
- <string>https://github.com/secondlife-3p/3p-velopack/releases/download/v0.0.1535-r2/velopack-40232ef.24685318450-darwin64-24685318450.tar.zst</string>
+ <string>https://github.com/secondlife-3p/3p-velopack/releases/download/v1.2.161/velopack-92d6a1c.39500165822-darwin64-39500165822.tar.zst</string>
</map>
<key>name</key>
<string>darwin64</string>
@@ -2783,7 +2783,7 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
<key>copyright</key>
<string>Velopack Ltd.</string>
<key>version</key>
- <string>40232ef.24685318450</string>
+ <string>v1.2.161.92d6a1c.39500165822</string>
<key>name</key>
<string>velopack</string>
<key>description</key>
diff --git a/indra/llfilesystem/lldir.cpp b/indra/llfilesystem/lldir.cpp
index 4094e7a87f..543760da47 100644
--- a/indra/llfilesystem/lldir.cpp
+++ b/indra/llfilesystem/lldir.cpp
@@ -1165,6 +1165,8 @@ const char* const LLDir::PORTABLE_USER_DATA_DIRNAME = "UserData";
// static
bool LLDir::isPortableInstall(const std::string& install_root)
{
+ // Todo: portable mac builds don't have a .portable marker file.
+ // Fix mac portable builds to include one.
if (install_root.empty())
{
return false;
diff --git a/indra/llfilesystem/lldir_mac.cpp b/indra/llfilesystem/lldir_mac.cpp
index 906894868e..44ef2feefe 100644
--- a/indra/llfilesystem/lldir_mac.cpp
+++ b/indra/llfilesystem/lldir_mac.cpp
@@ -163,9 +163,8 @@ LLDir_Mac::LLDir_Mac()
// viewer is unaffected.if (!is_dmg && !is_translocated && !is_applications_install && !install_root.empty())
// macos doesn't have the .portable marker , but check it just in case.
+ // Todo: fix this, portable installs should have a marker file.
bool is_portable = isPortableInstall(install_root);
- // For a velopack build, assume that running nor from a dmg means 'portable'.
- is_portable |= !is_dmg && !is_translocated && !is_applications_install && !install_root.empty();
if (is_portable)
{
@@ -213,6 +212,10 @@ LLDir_Mac::LLDir_Mac()
mRunContextDescription = "Portable (extracted archive, " + install_root + ")";
}
#endif // LL_VELOPACK
+ else if (!is_dmg && !is_translocated && !is_applications_install && !install_root.empty())
+ {
+ mRunContextDescription = "Running from a non-standard folder (" + install_root + ")";
+ }
else if (!install_root.empty())
{
// Bundle is present on local disk, intact, and not translocated
diff --git a/indra/llwebrtc/CMakeLists.txt b/indra/llwebrtc/CMakeLists.txt
index ffea44d09d..2cc4278712 100644
--- a/indra/llwebrtc/CMakeLists.txt
+++ b/indra/llwebrtc/CMakeLists.txt
@@ -40,7 +40,8 @@ if (WINDOWS)
msdmo
strmiids
iphlpapi
- libcmt)
+ libcmt
+ crypt32)
# as the webrtc libraries are release, build this binary as release as well.
target_compile_options(llwebrtc PRIVATE "/MT" "/Zc:wchar_t")
if (USE_BUGSPLAT)
@@ -54,6 +55,7 @@ elseif (DARWIN)
"-framework CoreMedia"
"-framework CoreVideo"
"-framework Foundation"
+ "-framework Security"
)
if (USE_BUGSPLAT)
set_target_properties(llwebrtc PROPERTIES XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT "dwarf-with-dsym"
diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp
index 15eee9b39c..d07f801051 100644
--- a/indra/llwebrtc/llwebrtc.cpp
+++ b/indra/llwebrtc/llwebrtc.cpp
@@ -59,13 +59,15 @@ webrtc::LossBasedBweV2::Config::Config()
namespace llwebrtc
{
#if WEBRTC_WIN
-static int16_t PLAYOUT_DEVICE_DEFAULT = webrtc::AudioDeviceModule::kDefaultCommunicationDevice;
-static int16_t RECORD_DEVICE_DEFAULT = webrtc::AudioDeviceModule::kDefaultCommunicationDevice;
+static int16_t PLAYOUT_DEVICE_DEFAULT = webrtc::AudioDeviceModule::kDefaultDevice;
+static int16_t RECORD_DEVICE_DEFAULT = webrtc::AudioDeviceModule::kDefaultDevice;
#else
static int16_t PLAYOUT_DEVICE_DEFAULT = 0;
static int16_t RECORD_DEVICE_DEFAULT = 0;
#endif
+constexpr int32_t MIC_CAPTURE_STOP_DELAY_MS = 40;
+
//
// LLWebRTCAudioTransport implementation
@@ -279,19 +281,18 @@ void LLWebRTCAudioDeviceModule::SetTuning(bool tuning, bool mute)
tuning_ = tuning;
if (tuning)
{
- // 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.
+ // Ensure capture is running so the mic-level meter works, and stop rendering
+ // the call while tuning. The recording calls are no-ops if capture is already
+ // active (it may not be, with "stop capture while muted" enabled).
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.
+ // On exit, capture is left running here; with "stop capture while muted" the
+ // caller (setTuningMode) stops it again if we're muted or have no connection.
+ // Playout is restored by the caller via workerOpenPlayout(), keeping it gated
+ // on there being a connection to render.
}
//
@@ -656,16 +657,21 @@ void LLWebRTCImpl::unsetDevicesObserver(LLWebRTCDevicesObserver *observer)
}
// 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).
+// starts recording. By default capture runs the whole time voice is enabled
+// (never stopped for mute or between calls, so the AEC never cold-starts -- no
+// hiss on unmute), so this is a no-op when already recording. With "stop capture
+// while muted" it's only started while unmuted and once a connection's audio is
+// established. Device changes go through workerDeployDevices(), which stops
+// recording first to force a clean re-select; voice off goes through setVoiceEnabled(false).
void LLWebRTCImpl::workerStartRecording()
{
// 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())
+ // it's already running (that would cause the unmute hiss). With "stop
+ // capture while muted", also hold it off while muted or with no connection's
+ // audio is established.
+ if (!mDeviceModule || !mVoiceEnabled || mDeviceModule->Recording() ||
+ (mStopCaptureOnMute.load() && !mTuningMode &&
+ (mMute || !mAudioEstablished)))
{
return;
}
@@ -707,7 +713,9 @@ void LLWebRTCImpl::workerStartRecording()
// disable before InitRecording so the recording stream is configured to
// use only WebRTC's software APM.
workerDisableBuiltInAudioProcessing();
- mDeviceModule->InitRecording();
+ // With "stop capture while muted", capture as media; otherwise as communications.
+ mDeviceModule->ForceInitRecording(mStopCaptureOnMute.load() ? webrtc::AudioDeviceModule::AudioCategory::kMedia
+ : webrtc::AudioDeviceModule::AudioCategory::kCommunications);
mDeviceModule->ForceStartRecording();
}
@@ -857,9 +865,9 @@ void LLWebRTCImpl::setVoiceEnabled(bool enable)
}
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.
+ // Voice on: start the capture device (by default it then stays
+ // running across calls and mute/unmute), and start playout if
+ // there's already a connection to render.
mDeviceModule->Init();
workerDeployDevices();
}
@@ -874,6 +882,33 @@ void LLWebRTCImpl::setVoiceEnabled(bool enable)
});
}
+void LLWebRTCImpl::setStopCaptureOnMute(bool stop)
+{
+ if (mStopCaptureOnMute.exchange(stop) == stop)
+ {
+ return;
+ }
+
+ mWorkerThread->PostTask([this, stop]
+ {
+ if (!mVoiceEnabled || !mDeviceModule || mTuningMode)
+ {
+ return;
+ }
+ if (stop)
+ {
+ if (mMute || !mAudioEstablished)
+ {
+ intSetMute(true, MIC_CAPTURE_STOP_DELAY_MS);
+ }
+ }
+ else
+ {
+ intSetMute(false, 0);
+ }
+ });
+}
+
// updateDevices needs to happen on the worker thread.
void LLWebRTCImpl::updateDevices()
{
@@ -954,6 +989,11 @@ void LLWebRTCImpl::setTuningMode(bool enable)
mDeviceModule->SetTuning(mTuningMode, mMute);
if (!mTuningMode)
{
+ if (mStopCaptureOnMute.load() && (mMute || !mAudioEstablished))
+ {
+ intSetMute(true, MIC_CAPTURE_STOP_DELAY_MS);
+ }
+
// Restore playout after tuning, gated on there being a
// connection to render (so the output device isn't left
// spinning with no engine data).
@@ -1031,15 +1071,53 @@ 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
+ // disabled per connection (see LLWebRTCPeerConnectionImpl::setMute). By
+ // default the capture device 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.
+ // cold-start hiss on unmute. With "stop capture while muted", it's stopped
+ // after the mute delay and restarted on unmute (below).
if (mPeerCustomProcessor)
{
mPeerCustomProcessor->setGain(mMute ? 0.0f : mGain);
}
+
+ if (mute && !mStopCaptureOnMute.load())
+ {
+ return;
+ }
+
+ // Prevent a delayed stop from an earlier mute from stopping a newly restarted recording after a rapid mute/unmute.
+ static std::atomic<uint32_t> mute_sequence(0);
+ uint32_t current_sequence = ++mute_sequence;
+
+ auto setRecording = [this, current_sequence](bool start)
+ {
+ if (!mDeviceModule || !mVoiceEnabled || mTuningMode ||
+ current_sequence != mute_sequence.load())
+ {
+ return;
+ }
+
+ mDeviceModule->ForceStopRecording();
+ if (start)
+ {
+ mDeviceModule->ForceInitRecording(mStopCaptureOnMute.load() ? webrtc::AudioDeviceModule::AudioCategory::kMedia
+ : webrtc::AudioDeviceModule::AudioCategory::kCommunications);
+ mDeviceModule->ForceStartRecording();
+ }
+ };
+
+ if (mute)
+ {
+ mWorkerThread->PostDelayedTask([setRecording]
+ {
+ setRecording(false);
+ }, webrtc::TimeDelta::Millis(delay_ms));
+ }
+ else
+ {
+ mWorkerThread->PostTask([setRecording] { setRecording(true); });
+ }
}
//
@@ -1050,18 +1128,20 @@ LLWebRTCPeerConnectionInterface *LLWebRTCImpl::newPeerConnection()
{
webrtc::scoped_refptr<LLWebRTCPeerConnectionImpl> peerConnection = webrtc::scoped_refptr<LLWebRTCPeerConnectionImpl>(new webrtc::RefCountedObject<LLWebRTCPeerConnectionImpl>(mEnv));
peerConnection->init(this);
- if (mPeerConnections.empty())
+ bool first_connection = mPeerConnections.empty();
+ mPeerConnections.emplace_back(peerConnection);
+ if (first_connection)
{
intSetMute(mMute);
}
- mPeerConnections.emplace_back(peerConnection);
// 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.
+ // is actually established (see startPlayout()). Capture isn't started here
+ // either: with "stop capture while muted" it waits for audio to be
+ // established, and the first connection's intSetMute() above may only stop it.
peerConnection->enableSenderTracks(false);
peerConnection->resetMute();
@@ -1078,10 +1158,12 @@ void LLWebRTCImpl::freePeerConnection(LLWebRTCPeerConnectionInterface* peer_conn
mPeerConnections.erase(it);
if (mPeerConnections.empty())
{
+ mAudioEstablished = false;
intSetMute(true);
// 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
+ // Capture stays running while voice is enabled so it's ready -- with no
+ // cold-start hiss -- when the next call comes up, unless "stop capture while
+ // muted" is on (intSetMute(true) above stops it). 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
@@ -1107,17 +1189,27 @@ void LLWebRTCImpl::freePeerConnection(LLWebRTCPeerConnectionInterface* peer_conn
void LLWebRTCImpl::startPlayout()
{
- // 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.
+ // Called when a connection's audio is established. 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 normally follows voice-enabled state (setVoiceEnabled), so it's
+ // already running if voice is on. The exception is "stop capture while
+ // muted", where it's held off until unmuted and audio is established.
+ // workerStartRecording() is gated on mVoiceEnabled, so this can't re-open
+ // the mic during voice-disable teardown.
mWorkerThread->PostTask(
[this]()
{
+ if (mPeerConnections.empty())
+ {
+ return;
+ }
+ mAudioEstablished = true;
workerStartPlayout();
+ if (mStopCaptureOnMute.load())
+ {
+ workerStartRecording();
+ }
});
}
@@ -1607,9 +1699,10 @@ void LLWebRTCPeerConnectionImpl::OnConnectionChange(webrtc::PeerConnectionInterf
case webrtc::PeerConnectionInterface::PeerConnectionState::kConnected:
{
// 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
+ // (Capture normally follows voice-enabled state, so it's already running;
+ // startPlayout() starts it only for "stop capture while muted".)
+ // 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++;
diff --git a/indra/llwebrtc/llwebrtc.h b/indra/llwebrtc/llwebrtc.h
index 821400cfe8..13078e39ea 100644
--- a/indra/llwebrtc/llwebrtc.h
+++ b/indra/llwebrtc/llwebrtc.h
@@ -156,10 +156,14 @@ class LLWebRTCDeviceInterface
// 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.
+ // enabled, capture normally stays running across calls and mute/unmute so the
+ // AEC never cold-starts (no unmute hiss), unless setStopCaptureOnMute() is on;
+ // playout still only runs when there's a connection to render.
virtual void setVoiceEnabled(bool enable) = 0;
+ // When true, capture is stopped while muted (or with no connection) and
+ // restarted on unmute, so Bluetooth headsets aren't held in call mode.
+ // May cause a brief hiss on unmute.
+ virtual void setStopCaptureOnMute(bool stop) = 0;
// Device observers for device change callbacks.
virtual void setDevicesObserver(LLWebRTCDevicesObserver *observer) = 0;
diff --git a/indra/llwebrtc/llwebrtc_impl.h b/indra/llwebrtc/llwebrtc_impl.h
index 8bf72890cf..d644bbdc24 100644
--- a/indra/llwebrtc/llwebrtc_impl.h
+++ b/indra/llwebrtc/llwebrtc_impl.h
@@ -234,7 +234,7 @@ public:
int32_t SetRecordingDevice(WindowsDeviceType type) override { return inner_->SetRecordingDevice(type); }
// --- Init/start/stop (forward) ---
- int32_t InitPlayout() override { return inner_->InitPlayout(); }
+ int32_t InitPlayout(AudioCategory category = AudioCategory::kMedia) override { return inner_->InitPlayout(category); }
bool PlayoutIsInitialized() const override { return inner_->PlayoutIsInitialized(); }
int32_t StartPlayout() override {
if (tuning_) return 0; // For tuning, don't allow playout
@@ -243,7 +243,14 @@ public:
int32_t StopPlayout() override { return inner_->StopPlayout(); }
bool Playing() const override { return inner_->Playing(); }
- int32_t InitRecording() override { return inner_->InitRecording(); }
+ int32_t InitRecording(AudioCategory category = AudioCategory::kMedia) override
+ {
+ // Ignore webrtc.lib's InitRecording (sent when a send stream starts), as
+ // with StartRecording/StopRecording: it would leave capture initialised even
+ // while we hold capture off. The app initialises capture itself via ForceInitRecording.
+ return 0;
+ }
+ int32_t ForceInitRecording(AudioCategory category = AudioCategory::kMedia) { return inner_->InitRecording(category); }
bool RecordingIsInitialized() const override { return inner_->RecordingIsInitialized(); }
int32_t StartRecording() override {
// ignore start recording as webrtc.lib will
@@ -448,6 +455,7 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO
void setRenderDevice(const std::string& id) override;
void setVoiceEnabled(bool enable) override;
+ void setStopCaptureOnMute(bool stop) override;
void setTuningMode(bool enable) override;
float getTuningAudioLevel() override;
@@ -531,8 +539,9 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO
void freePeerConnection(LLWebRTCPeerConnectionInterface* peer_connection);
// 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
+ // on there being a connection to render). Capture normally follows
+ // voice-enabled state, not connection state; the exception is "stop capture
+ // while muted", where it's also started here if unmuted. Safe to call from
// any thread (work is posted to the worker thread).
void startPlayout();
@@ -582,6 +591,11 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO
// Whether voice is enabled; gates whether the capture/playout devices run.
bool mVoiceEnabled;
float mGain;
+ std::atomic<bool> mStopCaptureOnMute{ false };
+ // True once any connection's audio has been established, until the last
+ // connection is freed (not a count of currently-connected peers). Used to
+ // hold capture off until a call is actually up.
+ std::atomic<bool> mAudioEstablished{ false };
LLCustomProcessorStatePtr mPeerCustomProcessor;
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index 9dad7348a8..7907159397 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -17650,5 +17650,16 @@
<key>Value</key>
<integer>0</integer>
</map>
+ <key>StopMicCaptureWhenMuted</key>
+ <map>
+ <key>Comment</key>
+ <string>Check to stop microphone capture while muted. May cause a hiss on unmute but prevents bluetooth devices always being in voice mode.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>Boolean</string>
+ <key>Value</key>
+ <integer>1</integer>
+ </map>
</map>
</llsd>
diff --git a/indra/newview/llagentcamera.cpp b/indra/newview/llagentcamera.cpp
index 52db5c4415..cfaaadd7b3 100644
--- a/indra/newview/llagentcamera.cpp
+++ b/indra/newview/llagentcamera.cpp
@@ -1506,11 +1506,13 @@ void LLAgentCamera::updateCamera()
{
// Keep tracked-object translation exact and smooth only changes
// to the camera offset, as is done in avatar-relative mode.
+ // Skip during the focus-change pan indicated by mCameraAnimating, see #6467.
LLVector3d camera_pos_focus = camera_pos_global - mFocusGlobal;
LLVector3d last_camera_pos_focus =
mCameraSmoothingLastPositionGlobal - mCameraSmoothingLastFocusGlobal;
LLVector3d delta = camera_pos_focus - last_camera_pos_focus;
- if (mCameraSmoothingLastFocusValid &&
+ if (!mCameraAnimating &&
+ mCameraSmoothingLastFocusValid &&
delta.magVec() < MAX_CAMERA_SMOOTH_DISTANCE)
{
camera_pos_focus = lerp(last_camera_pos_focus, camera_pos_focus, smoothing);
diff --git a/indra/newview/llchatservicehistory.cpp b/indra/newview/llchatservicehistory.cpp
index b6f5882bb4..cba1ad1582 100644
--- a/indra/newview/llchatservicehistory.cpp
+++ b/indra/newview/llchatservicehistory.cpp
@@ -168,6 +168,19 @@ struct StateResult
bool cleanup_pending = false;
};
+struct ArchivePreparation
+{
+ bool success = false;
+ bool bytes_changed = false;
+ ArchiveScan summary;
+};
+
+struct ArchivePublication
+{
+ bool success = false;
+ ArchiveScan summary;
+};
+
struct HttpResult
{
S32 status = 0;
@@ -238,6 +251,7 @@ bool ownsRuntime(U32 epoch)
void requireRuntime(U32 epoch)
{
+ LLCoros::checkStop();
if (!ownsRuntime(epoch))
{
LLTHROW(LLCoros::Stopped("Chat Service account ended"));
@@ -245,14 +259,65 @@ void requireRuntime(U32 epoch)
}
template <typename Work>
+auto runOnQueue(U32 epoch, const LL::WorkQueue::ptr_t& queue, Work work) -> decltype(work())
+{
+ using Result = decltype(work());
+
+ // Post the job and hold our own promise/future rather than calling
+ // waitForResult(), which blocks unconditionally on future.get(). A wait
+ // may span logout; the calling manager does not control how much work is
+ // already queued ahead of it, so it must be able to abandon the wait on
+ // its own schedule instead of being held hostage by the queue's backlog.
+ if (!queue)
+ {
+ requireRuntime(epoch);
+ return Result();
+ }
+
+ auto promise = std::make_shared<LLCoros::Promise<Result>>();
+ // Capture the shared_ptr by value: the job may still be queued or running
+ // when this coroutine gives up on it below, so the promise must outlive
+ // this call frame.
+ const bool posted = queue->post(
+ [promise, work]() mutable
+ {
+ try
+ {
+ promise->set_value(work());
+ }
+ catch (...)
+ {
+ promise->set_exception(std::current_exception());
+ }
+ });
+ if (!posted)
+ {
+ requireRuntime(epoch);
+ return Result();
+ }
+
+ auto future{ LLCoros::getFuture(*promise) };
+ LLCoros::TempStatus st("waiting for chat service queued work");
+ // Poll with a bounded timeout instead of an unconditional future.get() so
+ // a logout (epoch change) is observed promptly, even if the posted job is
+ // stuck behind other work on the target queue. The job itself keeps
+ // running to completion in the background; if nobody is left to use its
+ // result, it's simply discarded when the epoch check throws Stopped below.
+while (future.wait_for(std::chrono::milliseconds(50)) != boost::fibers::future_status::ready)
+ {
+ // calls checkStop()
+ requireRuntime(epoch);
+ }
+ requireRuntime(epoch);
+ return future.get();
+}
+
+template <typename Work>
auto runStorage(U32 epoch, Work work) -> decltype(work())
{
// A storage wait may span logout. Unwind the manager before it can touch the
// replacement account or a resident reference from the previous login.
- LL::WorkQueue::ptr_t general = LL::WorkQueue::getInstance("General");
- auto result = general ? general->waitForResult(work) : decltype(work())();
- requireRuntime(epoch);
- return result;
+ return runOnQueue(epoch, LL::WorkQueue::getInstance("General"), std::move(work));
}
void postPresentation(const nullary_func_t& work)
@@ -650,23 +715,25 @@ bool ownedArtifactName(const std::string& name)
return suffix == ".csv" || suffix == ".csv.tmp" || suffix == ".csv.corrupt";
}
-bool prepareArchive(const std::string& path, const LLUUID& resident_id, const LLUUID& agent_id,
- U64 boundary, ArchiveScan& summary, bool& bytes_changed)
+ArchivePreparation prepareArchive(const std::string& path, const LLUUID& resident_id,
+ const LLUUID& agent_id, U64 boundary)
{
LLMutexLock lock(&sStorageMutex);
+ ArchivePreparation result;
// Classify the canonical under the same lock used by mutation and view scans.
bool canonical_exists = false;
if (!inspectRegular(path, canonical_exists))
{
- return false;
+ return result;
}
ArchiveScan scan;
if (scanArchive(path, agent_id, resident_id, boundary, 0, scan))
{
- summary = scan;
- return true;
+ result.success = true;
+ result.summary = scan;
+ return result;
}
if (scan.state == ARCHIVE_TORN)
{
@@ -679,25 +746,26 @@ bool prepareArchive(const std::string& path, const LLUUID& resident_id, const LL
!truncateArchive(path, scan.valid_prefix_bytes) ||
!scanArchive(path, agent_id, resident_id, boundary, 0, scan))
{
- summary.state = ARCHIVE_FAILED;
- return false;
+ result.summary.state = ARCHIVE_FAILED;
+ return result;
}
- bytes_changed = true;
- summary = scan;
- return true;
+ result.success = true;
+ result.bytes_changed = true;
+ result.summary = scan;
+ return result;
}
if (scan.state != ARCHIVE_CORRUPT)
{
- summary.state = ARCHIVE_FAILED;
- return false;
+ result.summary.state = ARCHIVE_FAILED;
+ return result;
}
// Preserve one structurally unusable canonical before a later full-window rebuild.
const std::string corrupt = path + ".corrupt";
if (LLFile::remove(corrupt) != 0)
{
- summary.state = ARCHIVE_FAILED;
- return false;
+ result.summary.state = ARCHIVE_FAILED;
+ return result;
}
U64 current_size = 0;
S64 current_mtime = 0;
@@ -705,21 +773,22 @@ bool prepareArchive(const std::string& path, const LLUUID& resident_id, const LL
current_size != scan.file_size || current_mtime != scan.file_mtime ||
LLFile::rename(path, corrupt) != 0)
{
- summary.state = ARCHIVE_FAILED;
- return false;
+ result.summary.state = ARCHIVE_FAILED;
+ return result;
}
- bytes_changed = true;
- summary = ArchiveScan();
- return true;
+ result.success = true;
+ result.bytes_changed = true;
+ result.summary = ArchiveScan();
+ return result;
}
-bool publishRows(const std::string& path, const std::vector<Row>& rows,
- bool append, ArchiveScan& resulting)
+ArchivePublication publishRows(const std::string& path, const std::vector<Row>& rows,
+ bool append, ArchiveScan resulting)
{
LLMutexLock lock(&sStorageMutex);
if (rows.empty())
{
- return true;
+ return {true, resulting};
}
// The captured summary is a private physical identity check. External changes
@@ -727,7 +796,7 @@ bool publishRows(const std::string& path, const std::vector<Row>& rows,
bool exists = false;
if (!inspectRegular(path, exists) || exists != (resulting.state != ARCHIVE_ABSENT))
{
- return false;
+ return {false, resulting};
}
if (exists)
{
@@ -736,7 +805,7 @@ bool publishRows(const std::string& path, const std::vector<Row>& rows,
if (!archiveStamp(path, file_size, file_mtime) ||
file_size != resulting.file_size || file_mtime != resulting.file_mtime)
{
- return false;
+ return {false, resulting};
}
}
@@ -752,7 +821,7 @@ bool publishRows(const std::string& path, const std::vector<Row>& rows,
}
if (!writeReplace(path, output.str(), false))
{
- return false;
+ return {false, resulting};
}
}
else
@@ -761,7 +830,7 @@ bool publishRows(const std::string& path, const std::vector<Row>& rows,
bool needs_separator = false;
if (LLFile::stat(path, &status) != 0 || !LLFile::isfile(path))
{
- return false;
+ return {false, resulting};
}
if (status.st_size)
{
@@ -772,7 +841,7 @@ bool publishRows(const std::string& path, const std::vector<Row>& rows,
llofstream output(path.c_str(), std::ios::binary | std::ios::app);
if (!output.is_open())
{
- return false;
+ return {false, resulting};
}
if (needs_separator)
{
@@ -786,7 +855,7 @@ bool publishRows(const std::string& path, const std::vector<Row>& rows,
output.close();
if (output.fail())
{
- return false;
+ return {false, resulting};
}
}
@@ -801,10 +870,10 @@ bool publishRows(const std::string& path, const std::vector<Row>& rows,
resulting.newest = rows.back().key;
if (!archiveStamp(path, resulting.file_size, resulting.file_mtime))
{
- return false;
+ return {false, resulting};
}
resulting.valid_prefix_bytes = resulting.file_size;
- return true;
+ return {true, resulting};
}
typedef std::pair<std::vector<LLUUID>, bool> initial_artifacts_t;
@@ -1227,14 +1296,12 @@ bool prepareResidentArchive(const LLUUID& id, U32 epoch)
const std::string path = archivePath(sRuntime.account_dir, id);
const LLUUID agent_id = sRuntime.agent_id;
const U64 boundary = sRuntime.deleted_before_ticks;
- bool changed = false;
- ArchiveScan summary;
- const bool prepared = runStorage(epoch,
- [path, id, agent_id, boundary, &summary, &changed]()
+ const ArchivePreparation preparation = runStorage(epoch,
+ [path, id, agent_id, boundary]()
{
- return prepareArchive(path, id, agent_id, boundary, summary, changed);
+ return prepareArchive(path, id, agent_id, boundary);
});
- if (!prepared)
+ if (!preparation.success)
{
return false;
}
@@ -1242,9 +1309,9 @@ bool prepareResidentArchive(const LLUUID& id, U32 epoch)
// Only this manager removes residents, so the epoch check preserves their
// identity across the worker wait. Repair invalidates both views and coverage.
Resident& current = sRuntime.residents.at(id);
- current.summary = summary;
+ current.summary = preparation.summary;
sRuntime.index_dirty = true;
- if (changed)
+ if (preparation.bytes_changed)
{
++current.archive_serial;
current.covered_token.clear();
@@ -1458,13 +1525,14 @@ void syncResident(const LLUUID& id, const CapabilityContext& context, U32 epoch)
bool changed = false;
if (!staged.empty())
{
- ArchiveScan resulting = resident.summary;
const bool append = resident.summary.state == ARCHIVE_VALID && resident.summary.has_oldest;
const std::string path = archivePath(sRuntime.account_dir, id);
- changed = runStorage(epoch, [path, staged, append, &resulting]()
+ const ArchivePublication publication = runStorage(epoch,
+ [path, staged, append, summary = resident.summary]()
{
- return publishRows(path, staged, append, resulting);
+ return publishRows(path, staged, append, summary);
});
+ changed = publication.success;
LL_INFOS("ChatServiceHistory")
<< "Archive publication: resident_id=" << id << ", rows=" << staged.size()
<< ", mode=" << (append ? "append" : "replace")
@@ -1476,7 +1544,7 @@ void syncResident(const LLUUID& id, const CapabilityContext& context, U32 epoch)
sRuntime.local_content_exists = true;
if (changed)
{
- resident.summary = resulting;
+ resident.summary = publication.summary;
if (resident.metadata.state == META_RESOLVED)
{
const F64 archived_seconds = resident.summary.newest.ticks >= UUID_EPOCH
@@ -1643,8 +1711,11 @@ void runDelete(U32 epoch)
// Await invalidation on the main loop before sweeping files or accepting new
// history. A late clear must not discard messages received after deletion.
- LL::WorkQueue::ptr_t main = LL::WorkQueue::getInstance("mainloop");
- const bool cleared = main && main->waitForResult([epoch]()
+ // Poll rather than block unconditionally: deletion does not control its own
+ // lifetime either, so a logout must unwind this wait promptly even if the
+ // main loop is slow to pick up the posted work (might never pick it up in
+ // case of an error state being set).
+ const bool cleared = runOnQueue(epoch, LL::WorkQueue::getInstance("mainloop"), [epoch]()
{
if (!ownsRuntime(epoch))
{
@@ -1874,6 +1945,7 @@ void manager(U32 epoch)
// and storage maintenance in priority order.
while (sRuntime.running && sRuntime.epoch == epoch)
{
+ LLCoros::checkStop();
if (sRuntime.delete_active)
{
runDelete(epoch);
diff --git a/indra/newview/llvoicewebrtc.cpp b/indra/newview/llvoicewebrtc.cpp
index a20f6b1da7..976f5bc42b 100644
--- a/indra/newview/llvoicewebrtc.cpp
+++ b/indra/newview/llvoicewebrtc.cpp
@@ -95,6 +95,7 @@ namespace {
const uint32_t SET_HIDDEN_RESTORE_DELAY_MS = 200; // 200 ms to unmute again after hiding during teleport
const uint32_t MUTE_FADE_DELAY_MS = 500; // 20ms fade followed by 480ms silence gets rid of the click just after unmuting.
// This is because the buffers and processing is cleared by the silence.
+ // With StopMicCaptureWhenMuted, this is also the delay before capture is actually stopped.
const F32 SPEAKING_AUDIO_LEVEL = 0.30;
@@ -260,6 +261,11 @@ void LLWebRTCVoiceClient::cleanupSingleton()
mAvatarNameCacheConnection.disconnect();
}
+ if (mStopMicCaptureOnMuteConnection.connected())
+ {
+ mStopMicCaptureOnMuteConnection.disconnect();
+ }
+
sShuttingDown = true;
if (mSession)
{
@@ -309,6 +315,21 @@ void LLWebRTCVoiceClient::initWebRTC()
});
}
+ // Connect StopMicCaptureWhenMuted control so the changes apply instantly
+ if (!mStopMicCaptureOnMuteConnection.connected())
+ {
+ mStopMicCaptureOnMuteConnection =
+ gSavedSettings.getControl("StopMicCaptureWhenMuted")->getSignal()->connect(
+ [this](LLControlVariable*, const LLSD& new_val, const LLSD&)
+ {
+ if (mWebRTCDeviceInterface)
+ {
+ mWebRTCDeviceInterface->setStopCaptureOnMute(new_val.asBoolean());
+ }
+ });
+ }
+ mWebRTCDeviceInterface->setStopCaptureOnMute(gSavedSettings.getBOOL("StopMicCaptureWhenMuted"));
+
refreshDeviceLists();
}
@@ -1736,14 +1757,20 @@ void LLWebRTCVoiceClient::setMuteMic(bool muted)
mMuteMic = muted;
- if (mIsInTuningMode)
+ if (mIsInTuningMode &&!gSavedSettings.getBOOL("StopMicCaptureWhenMuted"))
{
return;
}
if (mWebRTCDeviceInterface)
{
- mWebRTCDeviceInterface->setMute(muted, muted ? MUTE_FADE_DELAY_MS : 0); // delay for 40ms on mute to allow buffers to empty
+ // Delay on mute to let the fade and silence flush buffers (and, if enabled, before capture stops)
+ mWebRTCDeviceInterface->setMute(muted, muted ? MUTE_FADE_DELAY_MS : 0);
+ }
+
+ if (mIsInTuningMode)
+ {
+ return;
}
// when you're hidden, your mic is always muted.
diff --git a/indra/newview/llvoicewebrtc.h b/indra/newview/llvoicewebrtc.h
index ef7cf49f1c..fd01bccdc7 100644
--- a/indra/newview/llvoicewebrtc.h
+++ b/indra/newview/llvoicewebrtc.h
@@ -454,6 +454,7 @@ public:
static void predAvatarNameResolution(const LLWebRTCVoiceClient::sessionStatePtr_t &session, LLUUID id, std::string name);
boost::signals2::connection mAvatarNameCacheConnection;
+ boost::signals2::connection mStopMicCaptureOnMuteConnection;
private:
// init or restart the WebRTC device interface.
diff --git a/indra/newview/skins/default/xui/en/panel_preferences_sound.xml b/indra/newview/skins/default/xui/en/panel_preferences_sound.xml
index 18fd7fe2d3..cdc0ed157a 100644
--- a/indra/newview/skins/default/xui/en/panel_preferences_sound.xml
+++ b/indra/newview/skins/default/xui/en/panel_preferences_sound.xml
@@ -456,4 +456,45 @@
top_delta="25"
width="15"
height="15"/>
+ <text
+ type="string"
+ length="1"
+ follows="left|top"
+ layout="topleft"
+ left="15"
+ top_delta="30"
+ font.style="BOLD"
+ name="bluetooth_lbl"
+ height="15"
+ width="110">
+ Bluetooth only
+ </text>
+ <radio_group
+ control_name="StopMicCaptureWhenMuted"
+ follows="left|top"
+ height="60"
+ layout="topleft"
+ left="25"
+ name="bluetooth_behavior"
+ top_pad="8"
+ width="480">
+ <radio_item
+ height="20"
+ label="Better voice"
+ layout="topleft"
+ left="0"
+ name="better_voice"
+ value="0"
+ top="0"
+ width="480" />
+ <radio_item
+ height="20"
+ label="Better in-world audio"
+ layout="topleft"
+ left_delta="0"
+ name="better_audio"
+ value="1"
+ top_delta="24"
+ width="480" />
+ </radio_group>
</panel>