using System.Collections.Generic; using System.Text; using Unity.Netcode; using Unity.Services.Vivox; using Unity.XR.CoreUtils.Bindings.Variables; using UnityEngine; using UnityEngine.Android; using System.Collections; #if UNITY_EDITOR using UnityEditor; #endif namespace XRMultiplayer { /// /// Manages the Vivox Voice Chat functionality in the VR Multiplayer template. /// public class VoiceChatManager : MonoBehaviour { /// /// String used to notify the player that they need to enable microphone permissions. /// const string k_MicrophonePersmissionDialogue = "Microphone Permissions Required."; /// public static BindableVariable s_HasMicrophonePermission = new(false); /// /// Dictionary of all the 's in the voice chat. /// public static Dictionary m_PlayersDictionary = new(); /// /// This is the bindable variable for subscribing to the local player muting themselves. /// public IReadOnlyBindableVariable selfMuted { get => m_SelfMuted; } readonly BindableVariable m_SelfMuted = new(false); /// /// This is the bindable variable for subscribing to the connection status of the voice chat service. /// public IReadOnlyBindableVariable connectionStatus { get => m_ConnectionStatus; } readonly BindableVariable m_ConnectionStatus = new(); /// /// The chat capability of the channel, by default it should Audio Only. /// [SerializeField, Tooltip("The chat capability of the channel, by default it should Audio Only")] ChatCapability m_ChatCapability = ChatCapability.AudioOnly; /// /// Update frequency for audio callbacks. /// [SerializeField, Tooltip("Update frequency for audio callbacks")] ParticipantPropertyUpdateFrequency m_UpdateFrequency = ParticipantPropertyUpdateFrequency.TenPerSecond; /// /// The maximum distance from the listener that a speaker can be heard. /// public int AudibleDistance { get => m_AudibleDistance; set => m_AudibleDistance = value; } [Header("Voice Chat Properties")] [SerializeField, Tooltip("The maximum distance from the listener that a speaker can be heard.")] int m_AudibleDistance = 32; /// /// The distance from the listener within which a speaker’s voice is heard at its original volume, and beyond which the speaker's voice begins to fade. /// public int ConversationalDistance { get => m_ConversationalDistance; set => m_ConversationalDistance = value; } [SerializeField, Tooltip("The distance from the listener within which a speaker’s voice is heard at its original volume, and beyond which the speaker's voice begins to fade.")] int m_ConversationalDistance = 7; /// /// The strength of the audio fade effect as the speaker moves away from the listener past the conversational distance. /// public float AudioFadeIntensity { get => m_AudioFadeIntensity; set => m_AudioFadeIntensity = value; } [SerializeField, Tooltip("The strength of the audio fade effect as the speaker moves away from the listener past the conversational distance.")] float m_AudioFadeIntensity = 1.0f; /// /// The model that determines the distance falloff of the voice chat. /// /// The strength of the audio fade effect as the speaker moves away from the listener past the conversational distance. /// public AudioFadeModel AudioFadeModel { get => m_AudioFadeModel; set => m_AudioFadeModel = value; } [SerializeField, Tooltip("The model that determines the distance falloff of the voice chat.")] AudioFadeModel m_AudioFadeModel = AudioFadeModel.LinearByDistance; /// /// The minimum and maximum volume for the voice output. /// [SerializeField, Tooltip("The minimum and maximum volume for the voice output.")] Vector2 m_MinMaxVoiceOutputVolume = new Vector2(-10.0f, 10.0f); /// /// The minimum and maximum volume for the voice input. /// [SerializeField, Tooltip("The minimum and maximum volume for the voice input.")] Vector2 m_MinMaxVoiceInputVolume = new Vector2(-10.0f, 10.0f); /// /// The local participant in the voice chat. /// VivoxParticipant m_LocalParticpant; /// /// The current lobby id the player is connected to. /// string m_CurrentLobbyId; /// /// If the player is connected to a room. /// bool m_ConnectedToRoom; /// /// If the voice chat service is initialized. /// bool m_IsInitialized; const string k_DebugPrepend = "[Voice Chat Manager] "; /// private void Awake() { m_ConnectedToRoom = false; XRINetworkGameManager.CurrentConnectionState.Subscribe(ConnectionStateUpdated); XRINetworkGameManager.Connected.Subscribe(ConnectedToGame); } /// private void OnDestroy() { if (VivoxService.Instance != null) { VivoxService.Instance.LoggedIn -= LocalUserLoggedIn; UnbindParticipantEvents(); } } /// /// Callback for when the local player connection state is updated. /// /// Wether or not a player is connected. void ConnectedToGame(bool connected) { if (!m_IsInitialized) return; if (connected) { Login(XRINetworkGameManager.AuthenicationId, XRINetworkGameManager.Instance.lobbyManager.connectedLobby.Id); } else { LogOut(); } } void ConnectionStateUpdated(XRINetworkGameManager.ConnectionState connectionState) { if (!m_IsInitialized && connectionState == XRINetworkGameManager.ConnectionState.Authenticated) { Utils.Log($"{k_DebugPrepend}Initializing Voice Chat"); m_ConnectionStatus.Value = "Initializing Voice Service"; m_IsInitialized = true; EnableVoiceChat(); if (!Permission.HasUserAuthorizedPermission(Permission.Microphone)) { StartCoroutine(ShowPermissionsAfterDelay()); } else { MicrophonePermissionGranted(); } } } IEnumerator ShowPermissionsAfterDelay(float delay = 1.0f) { Utils.Log($"{k_DebugPrepend}Requesting Microphone Permissions"); PlayerHudNotification.Instance.ShowText("Requesting Microphone Permissions", 3.0f); yield return new WaitForSeconds(delay); PermissionCallbacks permissionCallbacks = new(); permissionCallbacks.PermissionDenied += PermissionDeniedCallback; permissionCallbacks.PermissionGranted += PermissionGrantedCallback; Permission.RequestUserPermission(Permission.Microphone, permissionCallbacks); } void PermissionGrantedCallback(string permissionName) { if (permissionName == Permission.Microphone) { MicrophonePermissionGranted(); } } void PermissionDeniedCallback(string permissionName) { if (permissionName == Permission.Microphone) { PlayerHudNotification.Instance.ShowText("Microphone Permissions Denied", 3.0f); } } void MicrophonePermissionGranted() { Utils.Log($"{k_DebugPrepend}Microphone Permissions Granted"); s_HasMicrophonePermission.Value = true; PlayerHudNotification.Instance.ShowText("Microphone Permissions Granted", 3.0f); } public async void EnableVoiceChat() { try { await VivoxService.Instance.InitializeAsync(); m_ConnectionStatus.Value = "Voice Service Initialized"; VivoxService.Instance.LoggedIn += LocalUserLoggedIn; BindToParticipantEvents(); } catch (System.Exception e) { #if UNITY_EDITOR EditorGUI.hyperLinkClicked += HyperlinkClicked; Utils.Log($"{k_DebugPrepend}Vivox Initialization Failed. Please check the Vivox Service Window Project Settings > Services > Vivox\n\n{e}", 2); #else Utils.Log($"{k_DebugPrepend}Vivox Initialization Failed.\n\n{e}", 2); #endif } } #if UNITY_EDITOR void HyperlinkClicked(EditorWindow window, HyperLinkClickedEventArgs args) { if(args.hyperLinkData.ContainsValue("OpenVivoxSettings")) { SettingsService.OpenProjectSettings("Project/Services/Vivox"); } } #endif public async void Login(string displayName, string roomCode) { m_CurrentLobbyId = roomCode; LoginOptions loginOptions = new() { DisplayName = displayName, ParticipantUpdateFrequency = m_UpdateFrequency }; if (VivoxService.Instance.IsLoggedIn) { Utils.Log($"{k_DebugPrepend}Logging out of Voice Chat"); m_ConnectionStatus.Value = "Logging out of Voice Chat"; await VivoxService.Instance.LogoutAsync(); } if (!VivoxService.Instance.IsLoggedIn) { Utils.Log($"{k_DebugPrepend}Logging In to room {roomCode} as {displayName}"); m_ConnectionStatus.Value = "Logging In To Voice Service"; await VivoxService.Instance.LoginAsync(loginOptions); } else { Utils.Log($"{k_DebugPrepend}Attempting to login to voice chat while already logged in.", 1); } } void LocalUserLoggedIn() { if (VivoxService.Instance.IsLoggedIn) { Utils.Log($"{k_DebugPrepend}Local User Logged In to Voice Chat."); m_ConnectionStatus.Value = "Joining Voice Channel"; ConnectToVoiceChannel(); } } public async void ConnectToVoiceChannel() { if (NetworkManager.Singleton.IsConnectedClient & !m_ConnectedToRoom) { Channel3DProperties properties = new(AudibleDistance, ConversationalDistance, AudioFadeIntensity, AudioFadeModel); Utils.Log($"{k_DebugPrepend}Joining Voice Channel: {m_CurrentLobbyId}, properties: {properties}"); await VivoxService.Instance.JoinPositionalChannelAsync(m_CurrentLobbyId, m_ChatCapability, properties); // Once connecting, make sure we are still in the game session, if not, disconnect from the voice chat. if (!NetworkManager.Singleton.IsConnectedClient) { Disconnect(); } } else { Utils.Log($"{k_DebugPrepend}Failed to join Voice Chat, Player is not connected to a game", 1); } } void BindToParticipantEvents() { VivoxService.Instance.ParticipantAddedToChannel += OnParticipantAdded; VivoxService.Instance.ParticipantRemovedFromChannel += OnParticipantRemoved; } void UnbindParticipantEvents() { VivoxService.Instance.ParticipantAddedToChannel -= OnParticipantAdded; VivoxService.Instance.ParticipantRemovedFromChannel -= OnParticipantRemoved; } async void DisconnectAsync() { m_ConnectionStatus.Value = "Leaving current channel"; await VivoxService.Instance.LeaveAllChannelsAsync(); } [ContextMenu("Reconnect")] public void Reconnect() { ReconnectAsync(); } async void ReconnectAsync() { m_ConnectionStatus.Value = "Leaving current channel"; await VivoxService.Instance.LeaveAllChannelsAsync(); if (VivoxService.Instance.IsLoggedIn) { ConnectToVoiceChannel(); } else { m_ConnectionStatus.Value = "Reconnecting to Voice Chat"; Login(XRINetworkGameManager.AuthenicationId, XRINetworkGameManager.Instance.lobbyManager.connectedLobby.Id); } } public void LogOut() { Utils.Log($"{k_DebugPrepend}Logging out of Voice Chat."); if (VivoxService.Instance.IsLoggedIn && m_ConnectedToRoom) { m_ConnectedToRoom = false; VivoxService.Instance.LeaveAllChannelsAsync(); VivoxService.Instance.LogoutAsync(); } m_PlayersDictionary.Clear(); } public void Set3DAudio(Transform localPlayerHeadTransform) { if (VivoxService.Instance.IsLoggedIn && VivoxService.Instance.ActiveChannels.Count > 0 && VivoxService.Instance.TransmittingChannels[0] == m_CurrentLobbyId) { VivoxService.Instance.Set3DPosition(localPlayerHeadTransform.position, localPlayerHeadTransform.position, localPlayerHeadTransform.forward, localPlayerHeadTransform.up, m_CurrentLobbyId); } } public void ToggleSelfMute(bool setManual = false, bool mutedOverrideValue = false) { if (Permission.HasUserAuthorizedPermission(Permission.Microphone)) { m_SelfMuted.Value = setManual ? mutedOverrideValue : !m_SelfMuted.Value; } else { m_SelfMuted.Value = false; } if (VivoxService.Instance.IsLoggedIn) { if (m_SelfMuted.Value) { VivoxService.Instance.MuteInputDevice(); } else { VivoxService.Instance.UnmuteInputDevice(); } } else { OfflinePlayerAvatar.muted = m_SelfMuted.Value; } if (!Permission.HasUserAuthorizedPermission(Permission.Microphone)) { PlayerHudNotification.Instance.ShowText(k_MicrophonePersmissionDialogue, 3.0f); } } public void SetInputVolume(float volume) { volume = Mathf.Clamp(volume, m_MinMaxVoiceInputVolume.x, m_MinMaxVoiceInputVolume.y); VivoxService.Instance.SetInputDeviceVolume((int)volume); // Since the slider goes to .001 percent, add a buffer to mute the mic if (volume <= (m_MinMaxVoiceInputVolume.x + .05f)) { ToggleSelfMute(true, true); } else { ToggleSelfMute(true, false); } } public void SetOutputVolume(float volume) { volume = Mathf.Clamp(volume, m_MinMaxVoiceOutputVolume.x, m_MinMaxVoiceOutputVolume.y); VivoxService.Instance.SetOutputDeviceVolume((int)volume); } void OnParticipantAdded(VivoxParticipant participant) { if (participant.IsSelf) { m_ConnectedToRoom = true; m_LocalParticpant = participant; m_SelfMuted.Value = false; XRINetworkPlayer.LocalPlayer.SetVoiceId(m_LocalParticpant.PlayerId); Utils.Log($"{k_DebugPrepend}Joined Voice Channel: {m_CurrentLobbyId}"); m_ConnectionStatus.Value = "Joined Voice Channel"; PlayerHudNotification.Instance.ShowText("Joined Voice Chat", 3.0f); } else { Utils.Log($"{k_DebugPrepend}Non-Local Player Joined Voice Channel: {participant.PlayerId}"); foreach (XRINetworkPlayer player in FindObjectsByType(FindObjectsSortMode.None)) { if (player.playerVoiceId == participant.PlayerId) { player.SetupVoicePlayer(); } } } } void OnParticipantRemoved(VivoxParticipant participant) { RemoveVivoxPlayer(participant.PlayerId); if (participant.IsSelf) { Utils.Log($"{k_DebugPrepend}Left Voice Channel: {m_CurrentLobbyId}"); m_ConnectionStatus.Value = "Left Voice Channel"; m_ConnectedToRoom = false; m_LocalParticpant = null; PlayerHudNotification.Instance.ShowText("Voice Chat Disconnected", 3.0f); } } public VivoxParticipant GetVivoxParticipantById(string participantPlayerId) { foreach (var participant in VivoxService.Instance.ActiveChannels[m_CurrentLobbyId]) { if (participantPlayerId == participant.PlayerId) return participant; } return null; } // Gets called as soon as participant ID is synced public static void AddNewVivoxPlayer(string participantID, XRINetworkPlayer networkPlayer) { if (!m_PlayersDictionary.ContainsKey(participantID)) { m_PlayersDictionary.Add(participantID, networkPlayer); } else { Utils.Log($"{k_DebugPrepend}Attempting to load multiple players with same id {participantID}", 1); } } public static void RemoveVivoxPlayer(string participantID) { if (participantID == XRINetworkPlayer.LocalPlayer.playerVoiceId) { Utils.Log($"{k_DebugPrepend}Local Player Left Voice Chat."); return; } if (m_PlayersDictionary.ContainsKey(participantID)) { m_PlayersDictionary.Remove(participantID); } } [ContextMenu("Disconnect")] public void Disconnect() { DisconnectAsync(); } [ContextMenu("Debug Particpants")] void DebugParticipants() { StringBuilder output = new StringBuilder(); output.Append($"[Room Type: Positional\n[Room Code: {m_CurrentLobbyId}]"); foreach (var participant in VivoxService.Instance.ActiveChannels[m_CurrentLobbyId]) { output.Append($"\n[ParticipantID: {participant.PlayerId}]\n[AudioEnergy: {participant.AudioEnergy}]"); } Utils.Log($"{k_DebugPrepend}{output}"); } } }