diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp index 76ca4abb25..1eb2d17807 100644 --- a/indra/llwebrtc/llwebrtc.cpp +++ b/indra/llwebrtc/llwebrtc.cpp @@ -1524,6 +1524,57 @@ void LLWebRTCPeerConnectionImpl::unsetDataObserver(LLWebRTCDataObserver* observe } } +class LLStatsCollectorCallback : public webrtc::RTCStatsCollectorCallback +{ +public: + typedef std::function StatsCallback; + + LLStatsCollectorCallback(StatsCallback callback) : callback_(callback) {} + + void OnStatsDelivered(const webrtc::scoped_refptr& report) override + { + if (callback_) + { + // Transform RTCStatsReport stats to simple map + LLWebRTCStatsMap stats_map; + for (const auto& stats : *report) + { + std::map stat_attributes; + + // Convert each attribute to string format + for (const auto& attribute : stats.Attributes()) + { + stat_attributes[attribute.name()] = attribute.ToString(); + } + stats_map[stats.id()] = stat_attributes; + } + callback_(stats_map); + } + } + +private: + StatsCallback callback_; +}; + +void LLWebRTCPeerConnectionImpl::gatherConnectionStats() +{ + if (!mPeerConnection) + { + return; + } + + auto stats_callback = webrtc::make_ref_counted( + [this](const LLWebRTCStatsMap& generic_stats) + { + for (auto& observer : mSignalingObserverList) + { + observer->OnStatsDelivered(generic_stats); + } + }); + + mPeerConnection->GetStats(stats_callback.get()); +} + LLWebRTCImpl * gWebRTCImpl = nullptr; LLWebRTCDeviceInterface * getDeviceInterface() { diff --git a/indra/llwebrtc/llwebrtc.h b/indra/llwebrtc/llwebrtc.h index 7d06b7d2b4..e76e708f0c 100644 --- a/indra/llwebrtc/llwebrtc.h +++ b/indra/llwebrtc/llwebrtc.h @@ -38,6 +38,7 @@ #ifndef LLWEBRTC_H #define LLWEBRTC_H +#include #include #include @@ -55,6 +56,7 @@ namespace llwebrtc { +typedef std::map> LLWebRTCStatsMap; class LLWebRTCLogCallback { @@ -240,6 +242,8 @@ class LLWebRTCSignalingObserver // Called when the data channel has been established and data // transfer can begin. virtual void OnDataChannelReady(LLWebRTCDataInterface *data_interface) = 0; + + virtual void OnStatsDelivered(const LLWebRTCStatsMap& stats_data) {} }; // LLWebRTCPeerConnectionInterface representsd a connection to a peer, @@ -273,6 +277,8 @@ class LLWebRTCPeerConnectionInterface virtual void unsetSignalingObserver(LLWebRTCSignalingObserver* observer) = 0; virtual void AnswerAvailable(const std::string &sdp) = 0; + + virtual void gatherConnectionStats() = 0; }; // The following define the dynamic linked library diff --git a/indra/llwebrtc/llwebrtc_impl.h b/indra/llwebrtc/llwebrtc_impl.h index 01cfb17ced..c1e909df72 100644 --- a/indra/llwebrtc/llwebrtc_impl.h +++ b/indra/llwebrtc/llwebrtc_impl.h @@ -648,6 +648,8 @@ class LLWebRTCPeerConnectionImpl : public LLWebRTCPeerConnectionInterface, void enableSenderTracks(bool enable); void enableReceiverTracks(bool enable); + void gatherConnectionStats() override; + protected: LLWebRTCImpl * mWebRTCImpl; diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 0c5f3f3fd9..d6f3085b5c 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -420,6 +420,7 @@ set(viewer_SOURCE_FILES llfloaterimnearbychat.cpp llfloaterimnearbychathandler.cpp llfloaterimnearbychatlistener.cpp + llnearbyvoicemoderation.cpp llnetmap.cpp llnotificationalerthandler.cpp llnotificationgrouphandler.cpp @@ -1103,6 +1104,7 @@ set(viewer_HEADER_FILES llnameeditor.h llnamelistctrl.h llnavigationbar.h + llnearbyvoicemoderation.h llnetmap.h llnotificationhandler.h llnotificationlistitem.h diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 3dbb5dca1f..b0558eac78 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -6082,6 +6082,39 @@ Value 0 + OpenDebugStatVoice + + Comment + Expand Voice (WebRTC) stats display + Persist + 1 + Type + Boolean + Value + 1 + + OpenDebugStatVoiceOutgoing + + Comment + Expand Outgoing audio (Voice) stats display + Persist + 1 + Type + Boolean + Value + 1 + + OpenDebugStatVoiceIncoming + + Comment + Expand Incoming audio (Voice) stats display + Persist + 1 + Type + Boolean + Value + 1 + OutBandwidth Comment diff --git a/indra/newview/llfloaterimcontainer.cpp b/indra/newview/llfloaterimcontainer.cpp index a0f2dbe197..00b9c0b052 100644 --- a/indra/newview/llfloaterimcontainer.cpp +++ b/indra/newview/llfloaterimcontainer.cpp @@ -57,6 +57,8 @@ #include "llsdserialize.h" #include "llviewermenu.h" // is_agent_mappable #include "llviewerobjectlist.h" +#include "llvoavatar.h" +#include "llnearbyvoicemoderation.h" const S32 EVENTS_PER_IDLE_LOOP_CURRENT_SESSION = 80; @@ -90,6 +92,7 @@ LLFloaterIMContainer::LLFloaterIMContainer(const LLSD& seed, const Params& param mAutoResize = false; LLTransientFloaterMgr::getInstance()->addControlView(LLTransientFloaterMgr::IM, this); + LLNearbyVoiceModeration::getInstance(); } LLFloaterIMContainer::~LLFloaterIMContainer() @@ -530,6 +533,23 @@ void LLFloaterIMContainer::idleUpdate() mGeneralTitleInUse = !needs_override; setTitle(needs_override ? conversation_floaterp->getTitle() : mGeneralTitle); } + const LLConversationItem* nearby_session = getSessionModel(LLUUID()); + if (nearby_session) + { + LLFolderViewModelItemCommon::child_list_t::const_iterator current_participant_model = nearby_session->getChildrenBegin(); + LLFolderViewModelItemCommon::child_list_t::const_iterator end_participant_model = nearby_session->getChildrenEnd(); + while (current_participant_model != end_participant_model) + { + LLConversationItemParticipant* participant_model = + dynamic_cast((*current_participant_model).get()); + if (participant_model) + { + participant_model->setModeratorOptionsVisible(LLNearbyVoiceModeration::getInstance()->isNearbyChatModerator()); + } + + current_participant_model++; + } + } } mParticipantRefreshTimer.setTimerExpirySec(1.0f); @@ -1685,6 +1705,10 @@ bool LLFloaterIMContainer::visibleContextMenuItem(const LLSD& userdata) { return isMuted(conversation_item->getUUID()); } + else if ("can_allow_text_chat" == item) + { + return !isNearbyChatSpeakerSelected(); + } return true; } @@ -2014,9 +2038,27 @@ LLConversationViewParticipant* LLFloaterIMContainer::createConversationViewParti bool LLFloaterIMContainer::enableModerateContextMenuItem(const std::string& userdata, bool is_self) { - // only group moderators can perform actions related to this "enable callback" - if (!isGroupModerator()) + if (LLNearbyVoiceModeration::getInstance()->isNearbyChatModerator() && isNearbyChatSpeakerSelected()) { + // Determine here which actions are allowed + if ("can_moderate_voice" == userdata) + { + return true; + } + else if (("can_mute" == userdata)) + { + return !is_self; + } + else if ("can_unmute" == userdata) + { + return true; + } + + return false; + } + else if (!isGroupModerator()) + { + // only group moderators can perform actions related to this "enable callback" return false; } @@ -2149,7 +2191,35 @@ void LLFloaterIMContainer::banSelectedMember(const LLUUID& participant_uuid) void LLFloaterIMContainer::moderateVoice(const std::string& command, const LLUUID& userID) { - if (!gAgent.getRegion()) return; + if (!gAgent.getRegion()) + { + return; + } + + if (isNearbyChatSpeakerSelected()) + { + if ("selected" == command) + { + // Request a mute/unmute using a capability request via the simulator + LLNearbyVoiceModeration::getInstance()->requestMuteIndividual(userID, !isMuted(userID)); + } + else + if ("mute_all" == command) + { + // Send the mute_all request to the server + const bool mute_state = true; + LLNearbyVoiceModeration::getInstance()->requestMuteAll(mute_state); + } + else + if ("unmute_all" == command) + { + // Send the unmute_all request to the server + const bool mute_state = false; + LLNearbyVoiceModeration::getInstance()->requestMuteAll(mute_state); + } + + return; + } if (command.compare("selected")) { @@ -2267,6 +2337,31 @@ LLSpeaker * LLFloaterIMContainer::getSpeakerOfSelectedParticipant(LLSpeakerMgr * return speaker_managerp->findSpeaker(participant_itemp->getUUID()); } +bool LLFloaterIMContainer::isNearbyChatSpeakerSelected() +{ + LLFolderViewItem *selectedItem = mConversationsRoot->getCurSelectedItem(); + if (!selectedItem) + { + LL_WARNS() << "Current selected item is null" << LL_ENDL; + return NULL; + } + + conversations_widgets_map::const_iterator iter = mConversationsWidgets.begin(); + conversations_widgets_map::const_iterator end = mConversationsWidgets.end(); + const LLUUID * conversation_uuidp = NULL; + while(iter != end) + { + if (iter->second == selectedItem || iter->second == selectedItem->getParentFolder()) + { + conversation_uuidp = &iter->first; + break; + } + ++iter; + } + // Nearby chat ID is LLUUID::null + return conversation_uuidp->isNull(); +} + void LLFloaterIMContainer::toggleAllowTextChat(const LLUUID& participant_uuid) { LLIMSpeakerMgr * speaker_managerp = dynamic_cast(getSpeakerMgrForSelectedParticipant()); diff --git a/indra/newview/llfloaterimcontainer.h b/indra/newview/llfloaterimcontainer.h index 30eed8be36..9f1690a9b9 100644 --- a/indra/newview/llfloaterimcontainer.h +++ b/indra/newview/llfloaterimcontainer.h @@ -178,6 +178,7 @@ class LLFloaterIMContainer void banSelectedMember(const LLUUID& participant_uuid); void openNearbyChat(); bool isParticipantListExpanded(); + bool isNearbyChatSpeakerSelected(); void idleUpdate(); // for convenience (self) from static idle void idleProcessEvents(); diff --git a/indra/newview/llnearbyvoicemoderation.cpp b/indra/newview/llnearbyvoicemoderation.cpp new file mode 100644 index 0000000000..a5ee0e9423 --- /dev/null +++ b/indra/newview/llnearbyvoicemoderation.cpp @@ -0,0 +1,220 @@ +/** + * @file llnearbyvoicemoderation.cpp + * + * $LicenseInfo:firstyear=2008&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" + +#include "llagent.h" +#include "llnotificationsutil.h" +#include "llviewerregion.h" +#include "llvoavatar.h" +#include "llvoicechannel.h" +#include "llvoiceclient.h" +#include "llviewerobjectlist.h" +#include "llviewerparcelmgr.h" +#include "roles_constants.h" + +#include "llnearbyvoicemoderation.h" + +LLNearbyVoiceModeration::LLNearbyVoiceModeration() +{ +} + +LLNearbyVoiceModeration::~LLNearbyVoiceModeration() +{ +} + +LLVOAvatar* LLNearbyVoiceModeration::getVOAvatarFromId(const LLUUID& agent_id) +{ + LLViewerObject *obj = gObjectList.findObject(agent_id); + while (obj && obj->isAttachment()) + { + obj = (LLViewerObject*)obj->getParent(); + } + + if (obj && obj->isAvatar()) + { + return (LLVOAvatar*)obj; + } + else + { + return NULL; + } +} + +const std::string LLNearbyVoiceModeration::getCapUrlFromRegion(LLViewerRegion* region) +{ + if (! region || ! region->capabilitiesReceived()) + { + return std::string(); + } + + std::string url = region->getCapability("SpatialVoiceModerationRequest"); + if (url.empty()) + { + LL_INFOS() << "Capability URL for region " << region->getName() << " is empty" << LL_ENDL; + return std::string(); + } + LL_INFOS() << "Capability URL for region " << region->getName() << " is " << url << LL_ENDL; + + return url; +} + +void LLNearbyVoiceModeration::requestMuteIndividual(const LLUUID& agent_id, bool mute) +{ + LLVOAvatar* avatar = getVOAvatarFromId(agent_id); + if (avatar) + { + const std::string cap_url = getCapUrlFromRegion(avatar->getRegion()); + if (cap_url.length()) + { + const std::string operand = mute ? "mute" : "unmute"; + + LLSD body; + body["operand"] = operand; + body["agent_id"] = agent_id; + + const std::string agent_name = avatar->getFullname(); + LL_INFOS() << "Resident " << agent_name + << " (" << agent_id << ")" << " applying " << operand << LL_ENDL; + + std::string success_msg = + STRINGIZE("Resident " << agent_name + << " (" << agent_id << ")" << " nearby voice was set to " << operand); + + std::string failure_msg = + STRINGIZE("Unable to change voice muting for resident " + << agent_name << " (" << agent_id << ")"); + + LLCoreHttpUtil::HttpCoroutineAdapter::messageHttpPost( + cap_url, + body, + success_msg, + failure_msg); + } + } +} + +void LLNearbyVoiceModeration::requestMuteAll(bool mute) +{ + // Use our own avatar to get the region name + LLViewerRegion* region = gAgent.getRegion(); + + const std::string cap_url = getCapUrlFromRegion(region); + if (cap_url.length()) + { + const std::string operand = mute ? "mute_all" : "unmute_all"; + + LLSD body; + body["operand"] = operand; + + LL_INFOS() << "For all residents in this region, applying: " << operand << LL_ENDL; + + std::string success_msg = + STRINGIZE("Nearby voice for all residents was set to: " << operand); + + std::string failure_msg = + STRINGIZE("Unable to set nearby voice for all residents to: " << operand); + + LLCoreHttpUtil::HttpCoroutineAdapter::messageHttpPost( + cap_url, + body, + success_msg, + failure_msg); + } +} + +void LLNearbyVoiceModeration::setMutedInfo(const std::string& channelID, bool mute) +{ + auto it = mChannelMuteMap.find(channelID); + if (it == mChannelMuteMap.end()) + { + if (mute) + { + // Channel is new and being muted + showMutedNotification(true); + } + mChannelMuteMap[channelID] = mute; + } + else + { + if (it->second != mute) + { + // Flag changed + showMutedNotification(mute); + it->second = mute; + } + } + if (mute && LLVoiceClient::getInstance()->getUserPTTState()) + { + LLVoiceClient::getInstance()->setUserPTTState(false); + } +} + +bool LLNearbyVoiceModeration::showNotificationIfNeeded() +{ + if (LLVoiceClient::getInstance()->inProximalChannel() && + LLVoiceClient::getInstance()->getIsModeratorMuted(gAgentID)) + { + return showMutedNotification(true); + } + return false; +} + +bool LLNearbyVoiceModeration::showMutedNotification(bool is_muted) +{ + // Check if the current voice channel is nearby chat + if (LLVoiceClient::getInstance()->inProximalChannel()) + { + LLNotificationsUtil::add(is_muted ? "NearbyVoiceMutedByModerator" : "NearbyVoiceUnmutedByModerator"); + return true; + } + return false; +} + +bool LLNearbyVoiceModeration::isNearbyChatModerator() +{ + // Region doesn't support WebRTC voice + if (!gAgent.getRegion() || !gAgent.getRegion()->isRegionWebRTCEnabled()) + { + return false; + } + + // Only show moderator options when connected to spatial voice chat + LLVoiceChannel* channel = LLVoiceChannel::getCurrentVoiceChannel(); + if (!channel || channel->getSessionID().notNull() || !LLAgent::isActionAllowed("speak")) + { + return false; + } + + if (LLViewerParcelMgr::getInstance()->isVoiceRestricted()) + { + // Only the parcel owner should have access to moderate parcel voice space + return LLViewerParcelMgr::getInstance()->allowVoiceModeration(); + } + else + { + return gAgent.canManageEstate(); + } +} diff --git a/indra/newview/llnearbyvoicemoderation.h b/indra/newview/llnearbyvoicemoderation.h new file mode 100644 index 0000000000..619f169883 --- /dev/null +++ b/indra/newview/llnearbyvoicemoderation.h @@ -0,0 +1,50 @@ +/** + * @file llnearbyvoicemoderation.h + * + * $LicenseInfo:firstyear=2008&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#pragma once + +class LLVOAvatar; + +class LLNearbyVoiceModeration : public LLSingleton { + LLSINGLETON(LLNearbyVoiceModeration); + ~LLNearbyVoiceModeration(); + + public: + void requestMuteIndividual(const LLUUID& userID, bool mute); + void requestMuteAll(bool mute); + + void setMutedInfo(const std::string& channelID, bool mute); + bool showMutedNotification(bool is_muted); + bool showNotificationIfNeeded(); + + bool isNearbyChatModerator(); + + private: + LLVOAvatar* getVOAvatarFromId(const LLUUID& id); + const std::string getCapUrlFromRegion(LLViewerRegion* region); + + boost::signals2::connection mParcelCallbackConnection; + std::map mChannelMuteMap; +}; diff --git a/indra/newview/llviewerparcelmgr.cpp b/indra/newview/llviewerparcelmgr.cpp index 432da2e990..452c666652 100644 --- a/indra/newview/llviewerparcelmgr.cpp +++ b/indra/newview/llviewerparcelmgr.cpp @@ -702,6 +702,16 @@ bool LLViewerParcelMgr::allowAgentVoice() const return allowAgentVoice(gAgent.getRegion(), mAgentParcel); } +bool LLViewerParcelMgr::isVoiceRestricted() const +{ + return mAgentParcel && !mAgentParcel->getParcelFlagUseEstateVoiceChannel(); +} + +bool LLViewerParcelMgr::allowVoiceModeration() const +{ + return isVoiceRestricted() && isParcelOwnedByAgent(mAgentParcel, GP_SESSION_MODERATOR); +} + bool LLViewerParcelMgr::allowAgentVoice(const LLViewerRegion* region, const LLParcel* parcel) const { return region && region->isVoiceEnabled() diff --git a/indra/newview/llviewerparcelmgr.h b/indra/newview/llviewerparcelmgr.h index 8439283eb0..0d79496ccc 100644 --- a/indra/newview/llviewerparcelmgr.h +++ b/indra/newview/llviewerparcelmgr.h @@ -174,6 +174,12 @@ class LLViewerParcelMgr : public LLSingleton bool allowAgentVoice() const; bool allowAgentVoice(const LLViewerRegion* region, const LLParcel* parcel) const; + // Returns true if this parcel is using private voice channel + bool isVoiceRestricted() const; + + // Can this agent moderate Nearby voice chat on this parcel? + bool allowVoiceModeration() const; + // Can this agent start flying on this parcel? // Used for parcel property icons in nav bar. bool allowAgentFly(const LLViewerRegion* region, const LLParcel* parcel) const; diff --git a/indra/newview/llviewerregion.cpp b/indra/newview/llviewerregion.cpp index cd70f8f9b9..edd262faa0 100755 --- a/indra/newview/llviewerregion.cpp +++ b/indra/newview/llviewerregion.cpp @@ -3300,6 +3300,7 @@ void LLViewerRegionImpl::buildCapabilityNames(LLSD& capabilityNames) capabilityNames.append("SetDisplayName"); capabilityNames.append("SimConsoleAsync"); capabilityNames.append("SimulatorFeatures"); + capabilityNames.append("SpatialVoiceModerationRequest"); capabilityNames.append("StartGroupProposal"); capabilityNames.append("TerrainNavMeshProperties"); capabilityNames.append("TextureStats"); @@ -3797,6 +3798,16 @@ std::string LLViewerRegion::getSimHostName() return std::string("..."); } + +bool LLViewerRegion::isRegionWebRTCEnabled() +{ + if (mSimulatorFeaturesReceived && mSimulatorFeatures.has("VoiceServerType")) + { + return mSimulatorFeatures["VoiceServerType"].asString() == "webrtc"; + } + return false; +} + void LLViewerRegion::applyCacheMiscExtras(LLViewerObject* obj) { LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY; diff --git a/indra/newview/llviewerregion.h b/indra/newview/llviewerregion.h index b3ec857907..fac18b28f4 100644 --- a/indra/newview/llviewerregion.h +++ b/indra/newview/llviewerregion.h @@ -424,6 +424,8 @@ class LLViewerRegion: public LLCapabilityProvider // implements this interface std::string getSimHostName(); + bool isRegionWebRTCEnabled(); + static bool isNewObjectCreationThrottleDisabled() {return sNewObjectCreationThrottle < 0;} // rebuild reflection probe list diff --git a/indra/newview/llviewerstats.cpp b/indra/newview/llviewerstats.cpp index d39d466205..db6d83db5f 100644 --- a/indra/newview/llviewerstats.cpp +++ b/indra/newview/llviewerstats.cpp @@ -263,6 +263,20 @@ LLTrace::SampleStatHandle > HUDS_FRAME_PCT("huds_ LLTrace::SampleStatHandle > UI_FRAME_PCT("ui_frame_pct"); LLTrace::SampleStatHandle > SWAP_FRAME_PCT("swap_frame_pct"); LLTrace::SampleStatHandle > IDLE_FRAME_PCT("idle_frame_pct"); + + + +LLTrace::SampleStatHandle WEBRTC_PACKETS_IN_LOST("webrtc_packets_in_lost", "Lost incoming packets"), + WEBRTC_PACKETS_IN_RECEIVED("webrtc_packets_in_recv", "Incoming packets received"), + WEBRTC_PACKETS_OUT_SENT("webrtc_packets_out_sent", "Outgoing packets sent"), + WEBRTC_PACKETS_OUT_LOST("webrtc_packets_out_lost", "Lost outgoing packets"); + +LLTrace::SampleStatHandle WEBRTC_JITTER_OUT("webrtc_jitter_out", "Timing variation of outgoing audio"), + WEBRTC_JITTER_IN("webrtc_jitter_in", "Timing variation of incoming audio"), + WEBRTC_LATENCY("webrtc_latency", "Round-trip audio delay"), + WEBRTC_UPLOAD_BANDWIDTH("webrtc_upload_bandwidth", "Estimated upload bandwidth"), + WEBRTC_JITTER_BUFFER("webrtc_jitter_buffer", "Average delay added to smooth incoming audio"); + } LLViewerStats::LLViewerStats() diff --git a/indra/newview/llviewerstats.h b/indra/newview/llviewerstats.h index 011269d7ee..92e15bb74b 100644 --- a/indra/newview/llviewerstats.h +++ b/indra/newview/llviewerstats.h @@ -229,6 +229,8 @@ extern LLTrace::EventStatHandle AVATAR_EDIT_TIME, extern LLTrace::EventStatHandle > OBJECT_CACHE_HIT_RATE; +extern LLTrace::SampleStatHandle WEBRTC_PACKETS_IN_LOST, WEBRTC_PACKETS_IN_RECEIVED, WEBRTC_PACKETS_OUT_SENT, WEBRTC_PACKETS_OUT_LOST; +extern LLTrace::SampleStatHandle WEBRTC_JITTER_OUT, WEBRTC_JITTER_IN, WEBRTC_LATENCY, WEBRTC_UPLOAD_BANDWIDTH, WEBRTC_JITTER_BUFFER; } class LLViewerStats : public LLSingleton diff --git a/indra/newview/llvoiceclient.cpp b/indra/newview/llvoiceclient.cpp index 71a9e71a9f..2d732d0a71 100644 --- a/indra/newview/llvoiceclient.cpp +++ b/indra/newview/llvoiceclient.cpp @@ -39,6 +39,7 @@ #include "llagent.h" #include "lltrans.h" #include "lluiusage.h" +#include "llnearbyvoicemoderation.h" const F32 LLVoiceClient::OVERDRIVEN_POWER_LEVEL = 0.7f; @@ -712,6 +713,13 @@ bool LLVoiceClient::getPTTIsToggle() void LLVoiceClient::inputUserControlState(bool down) { + if (down && !getUserPTTState()) + { + // Nearby chat is muted by moderator, don't toggle PTT + if (LLNearbyVoiceModeration::getInstance()->showNotificationIfNeeded()) + return; + } + if(mPTTIsToggle) { if(down) // toggle open-mic state on 'down' diff --git a/indra/newview/llvoicewebrtc.cpp b/indra/newview/llvoicewebrtc.cpp index 3efcd763e3..4af71c37d0 100644 --- a/indra/newview/llvoicewebrtc.cpp +++ b/indra/newview/llvoicewebrtc.cpp @@ -61,10 +61,12 @@ #include "llrand.h" #include "llviewerwindow.h" #include "llviewercamera.h" +#include "llviewerstats.h" #include "llversioninfo.h" #include "llviewernetwork.h" #include "llnotificationsutil.h" +#include "llnearbyvoicemoderation.h" #include "llcorehttputil.h" #include "lleventfilter.h" @@ -80,6 +82,8 @@ const std::string WEBRTC_VOICE_SERVER_TYPE = "webrtc"; +const F32 STATS_TIMER_DELAY = 2.0; + namespace { const F32 MAX_AUDIO_DIST = 50.0f; @@ -2904,6 +2908,7 @@ bool LLVoiceWebRTCConnection::connectionStateMachine() } mWebRTCAudioInterface->setReceiveVolume(mSpeakerVolume); LLWebRTCVoiceClient::getInstance()->OnConnectionEstablished(mChannelID, mRegionID); + resetConnectionStats(); setVoiceConnectionState(VOICE_STATE_WAIT_FOR_DATA_CHANNEL); break; } @@ -2953,6 +2958,13 @@ bool LLVoiceWebRTCConnection::connectionStateMachine() sendJoin(); } } + + static LLTimer stats_timer; + if (stats_timer.getElapsedTimeF32() > STATS_TIMER_DELAY) + { + mWebRTCPeerConnectionInterface->gatherConnectionStats(); + stats_timer.reset(); + } } break; } @@ -3169,12 +3181,54 @@ void LLVoiceWebRTCConnection::OnDataReceivedImpl(const std::string &data, bool b if (participant_obj.contains("m") && participant_obj["m"].is_bool()) { - participant->mIsModeratorMuted = participant_obj["m"].as_bool(); + bool is_moderator_muted = participant_obj["m"].as_bool(); + if (isSpatial()) + { + // ignore muted flags from non-primary server + if (mPrimary || primary) + { + participant->mIsModeratorMuted = is_moderator_muted; + if (gAgentID == agent_id) + { + LLNearbyVoiceModeration::getInstance()->setMutedInfo(mChannelID, is_moderator_muted); + } + } + } + else + { + participant->mIsModeratorMuted = is_moderator_muted; + } + } + } + } + else + { + if (isSpatial() && (mPrimary || primary)) + { + // mute info message can be received before join message, so try to mute again later + if (participant_obj.contains("m") && participant_obj["m"].is_bool()) + { + bool is_moderator_muted = participant_obj["m"].as_bool(); + std::string channel_id = mChannelID; + F32 delay { 1.5f }; + doAfterInterval( + [channel_id, agent_id, is_moderator_muted]() + { + LLWebRTCVoiceClient::participantStatePtr_t participant = + LLWebRTCVoiceClient::getInstance()->findParticipantByID(channel_id, agent_id); + if (participant) + { + participant->mIsModeratorMuted = is_moderator_muted; + if (gAgentID == agent_id) + { + LLNearbyVoiceModeration::getInstance()->setMutedInfo(channel_id, is_moderator_muted); + } + } + }, delay); } } } } - // tell the simulator to set the mute and volume data for this // participant, if there are any updates. boost::json::object root; @@ -3245,6 +3299,112 @@ void LLVoiceWebRTCConnection::sendJoin() mWebRTCDataInterface->sendData(json_data, false); } +void LLVoiceWebRTCConnection::OnStatsDelivered(const llwebrtc::LLWebRTCStatsMap& stats_data) +{ + LL::WorkQueue::postMaybe(mMainQueue, [=, this] + { + if (mShutDown) + { + return; + } + for (const auto& [stats_id, attributes] : stats_data) + { + if (attributes.contains("currentRoundTripTime")) + { + F32 rtt_seconds = 0.0f; + LLStringUtil::convertToF32(attributes.at("currentRoundTripTime"), rtt_seconds); + sample(LLStatViewer::WEBRTC_LATENCY, rtt_seconds * 1000.0f); + } + if (attributes.contains("availableOutgoingBitrate")) + { + F32 bitrate_bps = 0.0f; + LLStringUtil::convertToF32(attributes.at("availableOutgoingBitrate"), bitrate_bps); + sample(LLStatViewer::WEBRTC_UPLOAD_BANDWIDTH, bitrate_bps / 1000.0f); + } + + // Stat type detection below is heuristic-based. + // It's relied on specific fields to distinguish outbound-rtp, remote-inbound-rtp, and inbound-rtp. + // This approach works with current WebRTC stats but may need updating later. + + // Outbound RTP + if (attributes.contains("mediaSourceId")) + { + U32 out_packets_sent = 0; + LLStringUtil::convertToU32(attributes.at("packetsSent"), out_packets_sent); + sample(LLStatViewer::WEBRTC_PACKETS_OUT_SENT, out_packets_sent); + } + // Remote-Inbound RTP + else if (attributes.contains("localId")) + { + if (attributes.contains("packetsLost")) + { + U32 out_packets_lost = 0; + LLStringUtil::convertToU32(attributes.at("packetsLost"), out_packets_lost); + sample(LLStatViewer::WEBRTC_PACKETS_OUT_LOST, out_packets_lost); + } + if (attributes.contains("jitter")) + { + F32 jitter_seconds = 0.0f; + LLStringUtil::convertToF32(attributes.at("jitter"), jitter_seconds); + sample(LLStatViewer::WEBRTC_JITTER_OUT, jitter_seconds * 1000.0f); + } + } + // Inbound RTP + else if (attributes.contains("jitterBufferDelay")) + { + if (attributes.contains("packetsLost")) + { + U32 in_packets_lost = 0; + LLStringUtil::convertToU32(attributes.at("packetsLost"), in_packets_lost); + sample(LLStatViewer::WEBRTC_PACKETS_IN_LOST, in_packets_lost); + } + if (attributes.contains("packetsReceived")) + { + U32 in_packets_recv = 0; + LLStringUtil::convertToU32(attributes.at("packetsReceived"), in_packets_recv); + sample(LLStatViewer::WEBRTC_PACKETS_IN_RECEIVED, in_packets_recv); + } + if (attributes.contains("jitter")) + { + F32 jitter_seconds = 0.0f; + LLStringUtil::convertToF32(attributes.at("jitter"), jitter_seconds); + sample(LLStatViewer::WEBRTC_JITTER_IN, jitter_seconds * 1000.0f); + } + if (attributes.contains("jitterBufferDelay") && attributes.contains("jitterBufferEmittedCount")) + { + F32 total_delay_seconds = 0.0f; + F32 emitted_count_f = 0.0f; + + // total delay in seconds + LLStringUtil::convertToF32(attributes.at("jitterBufferDelay"), total_delay_seconds); + + // number of packets played out + LLStringUtil::convertToF32(attributes.at("jitterBufferEmittedCount"), emitted_count_f); + if (emitted_count_f > 0.0f) + { + F32 avg_delay_seconds = total_delay_seconds / emitted_count_f; + F32 avg_delay_ms = avg_delay_seconds * 1000.0f; + sample(LLStatViewer::WEBRTC_JITTER_BUFFER, avg_delay_seconds * 1000.0f); + } + } + } + } + }); +} + +void LLVoiceWebRTCConnection::resetConnectionStats() +{ + sample(LLStatViewer::WEBRTC_JITTER_BUFFER, 0); + sample(LLStatViewer::WEBRTC_JITTER_IN, 0); + sample(LLStatViewer::WEBRTC_JITTER_OUT, 0); + sample(LLStatViewer::WEBRTC_LATENCY, 0); + sample(LLStatViewer::WEBRTC_PACKETS_IN_LOST, 0); + sample(LLStatViewer::WEBRTC_PACKETS_IN_RECEIVED, 0); + sample(LLStatViewer::WEBRTC_PACKETS_OUT_SENT, 0); + sample(LLStatViewer::WEBRTC_PACKETS_OUT_LOST, 0); + sample(LLStatViewer::WEBRTC_UPLOAD_BANDWIDTH, 0); +} + ///////////////////////////// // WebRTC Spatial Connection diff --git a/indra/newview/llvoicewebrtc.h b/indra/newview/llvoicewebrtc.h index 2ce575852a..6786b049c2 100644 --- a/indra/newview/llvoicewebrtc.h +++ b/indra/newview/llvoicewebrtc.h @@ -540,6 +540,8 @@ class LLWebRTCVoiceClient : public LLSingleton, static bool sShuttingDown; LLEventMailDrop mWebRTCPump; + + LLSD mLastWebRTCStats; }; @@ -603,6 +605,8 @@ class LLVoiceWebRTCConnection : //@{ void OnDataReceived(const std::string &data, bool binary) override; void OnDataChannelReady(llwebrtc::LLWebRTCDataInterface *data_interface) override; + + void OnStatsDelivered(const llwebrtc::LLWebRTCStatsMap& stats_data) override; //@} void OnDataReceivedImpl(const std::string &data, bool binary); @@ -638,6 +642,8 @@ class LLVoiceWebRTCConnection : void OnVoiceConnectionRequestSuccess(const LLSD &body); + void resetConnectionStats(); + protected: typedef enum e_voice_connection_state { diff --git a/indra/newview/skins/default/xui/en/floater_stats.xml b/indra/newview/skins/default/xui/en/floater_stats.xml index 1600c422c3..191db4f854 100644 --- a/indra/newview/skins/default/xui/en/floater_stats.xml +++ b/indra/newview/skins/default/xui/en/floater_stats.xml @@ -418,6 +418,64 @@ + + + + + + + + + + + + + + + diff --git a/indra/newview/skins/default/xui/en/menu_conversation.xml b/indra/newview/skins/default/xui/en/menu_conversation.xml index 62cdaa5886..5a28f0dde5 100644 --- a/indra/newview/skins/default/xui/en/menu_conversation.xml +++ b/indra/newview/skins/default/xui/en/menu_conversation.xml @@ -176,57 +176,60 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml index dbd513afe8..14e9211b9c 100644 --- a/indra/newview/skins/default/xui/en/notifications.xml +++ b/indra/newview/skins/default/xui/en/notifications.xml @@ -9192,6 +9192,23 @@ Your voice has been muted by moderator. yestext="OK"/> + + The moderator has muted your voice chat. +People in this location will not hear you if you speak. + voice + + + + Your voice chat can now be heard by people in this location. + voice + +