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}");
}
}
}