Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0a14318
First commit for a second experiment, investigating how nearby voice/…
callumlinden Oct 9, 2025
219da2a
Remove separator bar for Nearby Voice
callumlinden Oct 9, 2025
53d8310
Improve robustness of when moderator options appear and add some init…
callumlinden Oct 13, 2025
c64c16a
Merge branch 'develop' of https://github.com/secondlife/viewer into c…
callumlinden Oct 20, 2025
21e9b38
Merge branch 'develop' of https://github.com/secondlife/viewer into c…
callumlinden Oct 23, 2025
cf048cf
#4013 Update voice moderator options; show notifications when muted; …
maxim-productengine Oct 30, 2025
c39135c
#4013 add simple voice moderation permission check
maxim-productengine Nov 7, 2025
74a64d2
Merge branch 'develop' into maxim/voice-moderation
maxim-productengine Nov 7, 2025
a4d01ed
Show moderator options only on webrtc region
maxim-productengine Nov 11, 2025
d9ec89a
Ignore muted flags from non-primary voice server
maxim-productengine Nov 12, 2025
8111052
#4994 remove redundant moderator_id key
maxim-productengine Nov 13, 2025
bee23b4
#4995 change muted/unmuted alerts to non-modal toast
maxim-productengine Nov 13, 2025
e740bd2
Toggle off 'Speak' button when muted by moderator
maxim-productengine Nov 14, 2025
6ee41d6
#5018 add webrtc connection statistics
maxim-productengine Nov 26, 2025
ec149b5
#5018 mac build fix
maxim-productengine Nov 26, 2025
a9e8676
#5055 don't show moderate menu if the user is not parcel owner within…
maxim-productengine Nov 27, 2025
1565e46
Merge branch 'develop' into maxim/voice-moderation
maxim-productengine Dec 1, 2025
8b1e44e
Merge branch 'develop' into maxim/voice-moderation
maxim-productengine Dec 2, 2025
88a3d95
#5088 Hide 'Moderation options' menu when disconnected from spatial v…
maxim-productengine Dec 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions indra/llwebrtc/llwebrtc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1524,6 +1524,57 @@ void LLWebRTCPeerConnectionImpl::unsetDataObserver(LLWebRTCDataObserver* observe
}
}

class LLStatsCollectorCallback : public webrtc::RTCStatsCollectorCallback
{
public:
typedef std::function<void(const LLWebRTCStatsMap&)> StatsCallback;

LLStatsCollectorCallback(StatsCallback callback) : callback_(callback) {}

void OnStatsDelivered(const webrtc::scoped_refptr<const webrtc::RTCStatsReport>& report) override
{
if (callback_)
{
// Transform RTCStatsReport stats to simple map
LLWebRTCStatsMap stats_map;
for (const auto& stats : *report)
{
std::map<std::string, std::string> 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<LLStatsCollectorCallback>(
[this](const LLWebRTCStatsMap& generic_stats)
{
for (auto& observer : mSignalingObserverList)
{
observer->OnStatsDelivered(generic_stats);
}
});

mPeerConnection->GetStats(stats_callback.get());
}

LLWebRTCImpl * gWebRTCImpl = nullptr;
LLWebRTCDeviceInterface * getDeviceInterface()
{
Expand Down
6 changes: 6 additions & 0 deletions indra/llwebrtc/llwebrtc.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
#ifndef LLWEBRTC_H
#define LLWEBRTC_H

#include <map>
#include <string>
#include <vector>

Expand All @@ -55,6 +56,7 @@

namespace llwebrtc
{
typedef std::map<std::string, std::map<std::string, std::string>> LLWebRTCStatsMap;

class LLWebRTCLogCallback
{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions indra/llwebrtc/llwebrtc_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,8 @@ class LLWebRTCPeerConnectionImpl : public LLWebRTCPeerConnectionInterface,
void enableSenderTracks(bool enable);
void enableReceiverTracks(bool enable);

void gatherConnectionStats() override;

protected:

LLWebRTCImpl * mWebRTCImpl;
Expand Down
2 changes: 2 additions & 0 deletions indra/newview/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ set(viewer_SOURCE_FILES
llfloaterimnearbychat.cpp
llfloaterimnearbychathandler.cpp
llfloaterimnearbychatlistener.cpp
llnearbyvoicemoderation.cpp
llnetmap.cpp
llnotificationalerthandler.cpp
llnotificationgrouphandler.cpp
Expand Down Expand Up @@ -1103,6 +1104,7 @@ set(viewer_HEADER_FILES
llnameeditor.h
llnamelistctrl.h
llnavigationbar.h
llnearbyvoicemoderation.h
llnetmap.h
llnotificationhandler.h
llnotificationlistitem.h
Expand Down
33 changes: 33 additions & 0 deletions indra/newview/app_settings/settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6082,6 +6082,39 @@
<key>Value</key>
<integer>0</integer>
</map>
<key>OpenDebugStatVoice</key>
<map>
<key>Comment</key>
<string>Expand Voice (WebRTC) stats display</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>Boolean</string>
<key>Value</key>
<integer>1</integer>
</map>
<key>OpenDebugStatVoiceOutgoing</key>
<map>
<key>Comment</key>
<string>Expand Outgoing audio (Voice) stats display</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>Boolean</string>
<key>Value</key>
<integer>1</integer>
</map>
<key>OpenDebugStatVoiceIncoming</key>
<map>
<key>Comment</key>
<string>Expand Incoming audio (Voice) stats display</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>Boolean</string>
<key>Value</key>
<integer>1</integer>
</map>
<key>OutBandwidth</key>
<map>
<key>Comment</key>
Expand Down
101 changes: 98 additions & 3 deletions indra/newview/llfloaterimcontainer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -90,6 +92,7 @@ LLFloaterIMContainer::LLFloaterIMContainer(const LLSD& seed, const Params& param

mAutoResize = false;
LLTransientFloaterMgr::getInstance()->addControlView(LLTransientFloaterMgr::IM, this);
LLNearbyVoiceModeration::getInstance();
}

LLFloaterIMContainer::~LLFloaterIMContainer()
Expand Down Expand Up @@ -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<LLConversationItemParticipant*>((*current_participant_model).get());
if (participant_model)
{
participant_model->setModeratorOptionsVisible(LLNearbyVoiceModeration::getInstance()->isNearbyChatModerator());
}

current_participant_model++;
}
}
}

mParticipantRefreshTimer.setTimerExpirySec(1.0f);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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"))
{
Expand Down Expand Up @@ -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<LLIMSpeakerMgr*>(getSpeakerMgrForSelectedParticipant());
Expand Down
1 change: 1 addition & 0 deletions indra/newview/llfloaterimcontainer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading