using System; using System.Collections; using System.Collections.Generic; using Epic.OnlineServices; using Epic.OnlineServices.Lobby; using Epic.OnlineServices.RTC; using Epic.OnlineServices.RTCAudio; using UnityEngine; /// /// Generic re-usable cross-platform class to provide common voice chat functionality based on the EOS Voice Chat service. /// This class does not know anything about EOS platform initialization or authentication; it just takes the required /// EOS interfaces and exposes a number of game-related voice functions. /// public class EOSVoiceChat { private const uint DefaultMaxChatPlayers = 16; // "Lobbies that generate conference rooms must have <= 16 max players" private readonly LobbyInterface lobbyInterface; private readonly RTCInterface rtcInterface; private readonly RTCAudioInterface audioInterface; private readonly Func productUserProvider; private string connectedLobbyId; public bool IsConnected => lobbyInterface != null && rtcInterface != null && audioInterface != null && !string.IsNullOrEmpty(connectedLobbyId); private ulong? onConnectionChangedCallbackId, onParticipantStatusChangedCallbackId, onParticipantUpdatedCallbackId; /// /// Provide the required interfaces for voice chat. Product User ID is provided through a callback, so that the /// same instance of this class can remain in use even if the logged in user changes. /// public EOSVoiceChat( LobbyInterface lobbyInterface, RTCInterface rtcInterface, RTCAudioInterface audioInterface, Func productUserProvider) { this.lobbyInterface = lobbyInterface; this.rtcInterface = rtcInterface; this.audioInterface = audioInterface; this.productUserProvider = productUserProvider; } /// /// Join an existing chat lobby or create a new one. /// The completion callback is invoked on both success and failure, and can be used to set up the chat lobby's initial parameters. /// public void ConnectToChat(string chatLobbyName, Action onCompleted = null, uint maxChatPlayers = DefaultMaxChatPlayers) { DisconnectChat(); // Leave any currently connected chat lobby var connectArgs = new ChatConnectArgs(chatLobbyName, onCompleted, maxChatPlayers); lobbyInterface.CreateLobbySearch(new CreateLobbySearchOptions { MaxResults = 1 }, out var searchHandle); searchHandle.SetLobbyId(new LobbySearchSetLobbyIdOptions { LobbyId = chatLobbyName }); var localUserId = productUserProvider.Invoke(); searchHandle.Find(new LobbySearchFindOptions { LocalUserId = localUserId }, null, findData => { switch (findData.ResultCode) { case Result.Success: searchHandle.CopySearchResultByIndex(new LobbySearchCopySearchResultByIndexOptions { LobbyIndex = 0 }, out var lobbyDetails); Debug.Log("Found existing chat lobby, joining..."); lobbyInterface.JoinLobby ( new JoinLobbyOptions { LocalUserId = localUserId, LobbyDetailsHandle = lobbyDetails, PresenceEnabled = false, }, connectArgs, HandleLobbyJoined ); break; default: Debug.Log($"Creating new chat lobby..."); lobbyInterface.CreateLobby ( new CreateLobbyOptions { LocalUserId = localUserId, AllowInvites = false, PermissionLevel = LobbyPermissionLevel.Publicadvertised, PresenceEnabled = false, MaxLobbyMembers = maxChatPlayers, DisableHostMigration = false, LobbyId = chatLobbyName, BucketId = Application.productName, // TODO: do we need anything more specific than this? EnableRTCRoom = true, }, connectArgs, HandleLobbyCreated ); break; } }); } private void HandleLobbyCreated(CreateLobbyCallbackInfo data) { var connectArgs = (ChatConnectArgs)data.ClientData; switch (data.ResultCode) { case Result.Success: connectedLobbyId = data.LobbyId; Debug.Log($"Chat lobby creation successful, lobby ID = {connectedLobbyId}"); connectArgs.onCompleted?.Invoke(true); SubscribeToRoomNotifications(); break; case Result.LobbyLobbyAlreadyExists: // This can happen if two clients try to create the same lobby at the same time, a classic race condition. // Try to join the other client's newly created chat lobby instead. connectedLobbyId = null; Debug.Log("Chat lobby already exists, attempting to join it..."); ConnectToChat(connectArgs.chatLobbyName, connectArgs.onCompleted, connectArgs.maxChatPlayers); break; default: connectedLobbyId = null; Debug.LogError($"Chat lobby creation failed, result code = {data.ResultCode}"); connectArgs.onCompleted?.Invoke(false); break; } } private void HandleLobbyJoined(JoinLobbyCallbackInfo data) { var connectArgs = (ChatConnectArgs)data.ClientData; switch (data.ResultCode) { case Result.Success: connectedLobbyId = data.LobbyId; Debug.Log($"Chat lobby joined successfully, lobby ID = {connectedLobbyId}"); connectArgs.onCompleted?.Invoke(true); SubscribeToRoomNotifications(); break; default: connectedLobbyId = null; Debug.LogError($"Chat lobby join failed, result code = {data.ResultCode}"); connectArgs.onCompleted?.Invoke(false); break; } } public void DisconnectChat() { if (!IsConnected) return; // Unsubscribing first means we don't get a Disconnected notification for intentionally leaving the lobby UnsubscribeFromRoomNotifications(); lobbyInterface.LeaveLobby ( new LeaveLobbyOptions { LocalUserId = productUserProvider.Invoke(), LobbyId = connectedLobbyId, }, null, data => { } ); connectedLobbyId = null; } /// /// Mute or unmute the local player's voice chat. This can be used to implement push-to-talk. /// public void SetLocalMuted(bool muted) { if (!IsConnected) return; // TODO: can also disable Sending of data entirely audioInterface.SetAudioInputSettings(new SetAudioInputSettingsOptions { LocalUserId = productUserProvider.Invoke(), DeviceId = null, // Default input device Volume = muted ? 0f : 100f, }); } /// /// Mute or unmute a specific remove player. This can be used to filter out specific players in the chat lobby, /// or for manually muting toxic players. /// public void SetRemoteMuted(string playerId, bool muted) { if (!IsConnected) return; } /// /// Set all remote players to muted. This can be used during loading screens, or to initialize the chat lobby /// when chatting with only a small subset of players. /// public void MuteAllRemote() { if (!IsConnected) return; } /// /// Set global voice chat volume. Values range between 0.0 (muted) and 1.0 (full volume). /// If desired, volume value can be increased up to 2.0 for an overdrive mode. /// public void SetOutputVolume(float volume) { audioInterface.SetAudioOutputSettings(new SetAudioOutputSettingsOptions { LocalUserId = productUserProvider.Invoke(), DeviceId = null, // Default output device Volume = volume * 50f, }); } // TODO: handle input/output device changes (keep track of current devices) // TODO: handle participant changes (add/remove users) => also map to platform-specific player IDs // TODO: query local/remote status (is talking or not) private void SubscribeToRoomNotifications() { var localUserId = productUserProvider.Invoke(); if (lobbyInterface.GetRTCRoomName( new GetRTCRoomNameOptions { LocalUserId = localUserId, LobbyId = connectedLobbyId }, out string roomName) != Result.Success) { Debug.LogError($"Failed to obtain RTC room name for lobby with ID {connectedLobbyId}"); return; } if (!onConnectionChangedCallbackId.HasValue) { onConnectionChangedCallbackId = lobbyInterface.AddNotifyRTCRoomConnectionChanged ( new AddNotifyRTCRoomConnectionChangedOptions { LocalUserId = localUserId, LobbyId = connectedLobbyId, }, null, HandleConnectionChanged ); } if (!onParticipantStatusChangedCallbackId.HasValue) { onParticipantStatusChangedCallbackId = rtcInterface.AddNotifyParticipantStatusChanged ( new AddNotifyParticipantStatusChangedOptions { LocalUserId = localUserId, RoomName = roomName, }, null, HandleParticipantStatusChanged ); } if (!onParticipantUpdatedCallbackId.HasValue) { onParticipantUpdatedCallbackId = audioInterface.AddNotifyParticipantUpdated ( new AddNotifyParticipantUpdatedOptions { LocalUserId = localUserId, RoomName = roomName, }, null, HandleParticipantUpdated ); } } private void UnsubscribeFromRoomNotifications() { if (onConnectionChangedCallbackId.HasValue) { rtcInterface.RemoveNotifyDisconnected(onConnectionChangedCallbackId.Value); onConnectionChangedCallbackId = null; } if (onParticipantStatusChangedCallbackId.HasValue) { rtcInterface.RemoveNotifyParticipantStatusChanged(onParticipantStatusChangedCallbackId.Value); onParticipantStatusChangedCallbackId = null; } if (onParticipantUpdatedCallbackId.HasValue) { audioInterface.RemoveNotifyParticipantUpdated(onParticipantUpdatedCallbackId.Value); onParticipantUpdatedCallbackId = null; } } private void HandleConnectionChanged(RTCRoomConnectionChangedCallbackInfo data) { Debug.Log($"RTC Room connection changed, connected = {data.IsConnected}, disconnect reason = {data.DisconnectReason}"); // Note: reconnecting is handled automatically by the lobby system } private void HandleParticipantStatusChanged(ParticipantStatusChangedCallbackInfo data) { Debug.Log($"Participant status changed, participant = {data.ParticipantId}, status = {data.ParticipantStatus}"); } private void HandleParticipantUpdated(ParticipantUpdatedCallbackInfo data) { Debug.Log($"Participant status changed, participant = {data.ParticipantId}, speaking = {data.Speaking}, audio status = {data.AudioStatus}"); } private class ChatConnectArgs { public readonly string chatLobbyName; public readonly Action onCompleted; public readonly uint maxChatPlayers; public ChatConnectArgs(string chatLobbyName, Action onCompleted, uint maxChatPlayers) { this.chatLobbyName = chatLobbyName; this.onCompleted = onCompleted; this.maxChatPlayers = maxChatPlayers; } } private class ChatUser { public readonly ProductUserId productUserId; public string platformPlayerId; } }