diff options
| author | Erik Kundiman <erik@megapahit.org> | 2026-10-22 11:41:46 +0800 |
|---|---|---|
| committer | Erik Kundiman <erik@megapahit.org> | 2026-10-22 11:41:46 +0800 |
| commit | 80e7d54b419b591a0dbb2a86a413e29f87c7e17e (patch) | |
| tree | ebf7fbcd3ef2ab3c5d2cd189a31dc6c5b8082dd6 /indra | |
| parent | dc3524d24ae63c1024c094b67fb13f41f49d5637 (diff) | |
| parent | d567465ea17a2523b200fbf6213adaacfccd23da (diff) | |
Merge tag 'Second_Life_Release#d567465e-26.5' into 26.5
Diffstat (limited to 'indra')
| -rw-r--r-- | indra/llfilesystem/lldir.cpp | 2 | ||||
| -rw-r--r-- | indra/llfilesystem/lldir_mac.cpp | 7 | ||||
| -rw-r--r-- | indra/llwebrtc/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | indra/llwebrtc/llwebrtc.cpp | 175 | ||||
| -rw-r--r-- | indra/llwebrtc/llwebrtc.h | 10 | ||||
| -rw-r--r-- | indra/llwebrtc/llwebrtc_impl.h | 22 | ||||
| -rw-r--r-- | indra/newview/app_settings/settings.xml | 11 | ||||
| -rw-r--r-- | indra/newview/llagentcamera.cpp | 4 | ||||
| -rw-r--r-- | indra/newview/llchatservicehistory.cpp | 168 | ||||
| -rw-r--r-- | indra/newview/llvoicewebrtc.cpp | 31 | ||||
| -rw-r--r-- | indra/newview/llvoicewebrtc.h | 1 | ||||
| -rw-r--r-- | indra/newview/skins/default/xui/en/panel_preferences_sound.xml | 41 |
12 files changed, 374 insertions, 102 deletions
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> |
