Participant management

The Vivox SDK posts information about individual participants in a channel that is visible to all other participants. This includes the following information:

  • When a user joins a channel.
  • When a user leaves a channel.
  • When there is an important change in user state, such as whether the user is speaking or typing.

Handling participant events is optional. The game can ignore these events if there is no visualization of the user state (for example, displaying who has voice enabled).

To provide a visualization of user state information, a game must handle the following messages:

  • VivoxService.Instance.ParticipantAddedToChannel
  • VivoxService.Instance.ParticipantRemovedFromChannel
  • VivoxParticipant.ParticipantMuteStateChanged
  • VivoxParticipant.ParticipantSpeechDetected
  • VivoxParticipant.ParticipantAudioEnergyChanged

VivoxParticipant

ParticipantAddedToChannel and ParticipantRemovedFromChannel both come with a VivoxParticipant. VivoxParticipant contains information about the participant that was just added, such as:

  • PlayerId
  • DisplayName
  • ChannelName of the channel the VivoxParticipant is a member of.
  • Whether the VivoxParticipant IsSelf - the participant representing the local player within the channel.

VivoxParticipant also contains the current state of that participant, including:

  • The IsMuted state
  • AudioEnergy
  • SpeechDetected (whether the AudioEnergy of the player has increased to the point that Vivox considers it speech).

VivoxParticipants should be tightly coupled to the UI representation of the participant with VivoxParticipant.ParticipantMuteStateChanged and VivoxParticipant.ParticipantSpeechDetected to convey whether the local player has the participant muted, or if the participant is currently speaking in the channel.

You can use VivoxParticipant.ParticipantAudioEnergyChanged to create a more accurate volume unit (VU) meter then SpeechDetected allows for.

The following code, which is a simplified segment from the Vivox ChatChannelSample, is an example of these systems:

public class RosterManager : MonoBehaviour
{
    private const string LobbyChannelName = "lobbyChannel";
    private Dictionary<string, List<RosterItem>> rosterObjects = new Dictionary<string, List<RosterItem>>();
    public GameObject rosterItemPrefab;


    private void Start()
    {
        VivoxService.Instance.ParticipantAddedToChannel += OnParticipantAdded;
        VivoxService.Instance.ParticipantRemovedFromChannel += OnParticipantRemoved;
    }

    public void ClearAllRosters()
    {
        foreach(List<RosterItem> rosterList in rosterObjects.Values)
        {
            foreach(RosterItem item in rosterList)
            {
                Destroy(item.gameObject);
            }
            rosterList.Clear();
        }
        rosterObjects.Clear();
    }

    public void ClearChannelRoster(string channelName)
    {
        List<RosterItem> rosterList = rosterObjects[channelName];
        foreach(RosterItem item in rosterList)
        {
            Destroy(item.gameObject);
        }
        rosterList.Clear();
        rosterObjects.Remove(channelName);
    }

    private void CleanRoster(string channelName)
    {
        RectTransform rt = this.gameObject.GetComponent<RectTransform>();
        rt.sizeDelta = new Vector2(0, rosterObjects[channelName].Count * 50);
    }

    void UpdateParticipantRoster(VivoxParticipant participant, bool isAddParticipant)
    {
        if (isAddParticipant)
        {
            GameObject newRosterObject = GameObject.Instantiate(rosterItemPrefab, this.gameObject.transform);
            RosterItem newRosterItem = newRosterObject.GetComponent<RosterItem>();
            List<RosterItem> thisChannelList;

            if (rosterObjects.ContainsKey(participant.ChannelName))
            {
                //Add this object to an existing roster
                rosterObjects.TryGetValue(participant.ChannelName, out thisChannelList);
                newRosterItem.SetupRosterItem(participant);
                thisChannelList.Add(newRosterItem);
                rosterObjects[participant.ChannelName] = thisChannelList;
            }
            else
            {
                //Create a new roster to add this object to
                thisChannelList = new List<RosterItem>();
                thisChannelList.Add(newRosterItem);
                newRosterItem.SetupRosterItem(participant);
                rosterObjects.Add(participant.ChannelName, thisChannelList);
            }
            CleanRoster(participant.ChannelName);
        }
        else
        {
            if (rosterObjects.ContainsKey(participant.ChannelName))
            {
                RosterItem removedItem = rosterObjects[participant.ChannelName].FirstOrDefault(p => p.Participant.PlayerId == participant.PlayerId);
                if (removedItem != null)
                {
                    rosterObjects[participant.ChannelName].Remove(removedItem);
                    Destroy(removedItem.gameObject);
                    CleanRoster(participant.ChannelName);
                }
                else
                {
                    Debug.LogError("Trying to remove a participant that has no roster item.");
                }
            }
        }
    }

    void OnParticipantAdded(VivoxParticipant participant)
    {
        UpdateParticipantRoster(participant, true);
    }

    void OnParticipantRemoved(VivoxParticipant participant)
    {
        UpdateParticipantRoster(participant, false);
    }
}

public class RosterItem : MonoBehaviour
{
    // Player specific items.
    public VivoxParticipant Participant;
    public Text PlayerNameText;

    public Image ChatStateImage;
    public Sprite MutedImage;
    public Sprite SpeakingImage;
    public Sprite NotSpeakingImage;

    Button m_muteButton;

    private void UpdateChatStateImage()
    {
        if (Participant.IsMuted)
        {
            ChatStateImage.sprite = MutedImage;
        }
        else
        {
            if (Participant.SpeechDetected)
            {
                ChatStateImage.sprite = SpeakingImage;
            }
            else
            {
                ChatStateImage.sprite = NotSpeakingImage;
            }
        }
    }

    public void SetupRosterItem(VivoxParticipant participant)
    {
        //Set the Participant variable of this RosterItem to the VivoxParticipant added in the RosterManager
        Participant = participant;
        PlayerNameText.text = Participant.DisplayName;
        // Update the image to the active state of the user (either the SpeakingImage, the MutedImage, or the NotSpeakingImage) and then attach
        // the function to run if an event is fired denoting a change to that users state
        UpdateChatStateImage();
        Participant.ParticipantMuteStateChanged += UpdateChatStateImage;
        Participant.ParticipantSpeechDetected += UpdateChatStateImage;

        //A button on the UI element itself is implemented to handle muting on the participant represented by the UI element
        m_muteButton = gameObject.GetComponent<Button>();
        m_muteButton.onClick.AddListener(() =>
        {
            // If already muted, unmute, and vice versa.
            if (Participant.IsMuted)
            {
                Participant.UnmutePlayerLocally();
            }
            else
            {
                Participant.MutePlayerLocally();
            }
        });
    }

    void OnDestroy()
    {
        Participant.ParticipantMuteStateChanged -= UpdateChatStateImage;
        Participant.ParticipantSpeechDetected -= UpdateChatStateImage;

        m_muteButton.onClick.RemoveAllListeners();
    }
}