You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
479 lines
17 KiB
479 lines
17 KiB
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;
|
|
|
|
/// <summary>
|
|
/// Generic reusable 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.
|
|
/// </summary>
|
|
public class EOSVoiceChat: IDisposable
|
|
{
|
|
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<ProductUserId> localUserProvider;
|
|
|
|
private readonly Dictionary<ProductUserId, ChatUser> chatUsers = new Dictionary<ProductUserId, ChatUser>();
|
|
|
|
private string connectedLobbyId;
|
|
private string rtcRoomName;
|
|
public bool IsConnected => lobbyInterface != null && rtcInterface != null && audioInterface != null && !string.IsNullOrEmpty(connectedLobbyId);
|
|
|
|
private string defaultInputDeviceId;
|
|
public bool HasInputDevice => !string.IsNullOrEmpty(defaultInputDeviceId);
|
|
|
|
private ulong? onAudioDevicesChangedCallbackId;
|
|
private ulong? onConnectionChangedCallbackId, onParticipantStatusChangedCallbackId, onParticipantUpdatedCallbackId;
|
|
|
|
public event Action OnChatConnected;
|
|
public event Action OnChatConnectionFailed;
|
|
public event Action OnChatDisconnected;
|
|
public event Action<ProductUserId> OnChatUserJoined;
|
|
public event Action<ProductUserId> OnChatUserLeft;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public EOSVoiceChat(
|
|
LobbyInterface lobbyInterface, RTCInterface rtcInterface, RTCAudioInterface audioInterface,
|
|
Func<ProductUserId> localUserProvider)
|
|
{
|
|
this.lobbyInterface = lobbyInterface;
|
|
this.rtcInterface = rtcInterface;
|
|
this.audioInterface = audioInterface;
|
|
this.localUserProvider = localUserProvider;
|
|
|
|
SubscribeToAudioDeviceNotifications();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
DisconnectChat();
|
|
UnsubscribeFromAudioDeviceNotifications();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Join an existing chat lobby or create a new one.
|
|
/// </summary>
|
|
public void ConnectToChat(string chatLobbyName, uint maxChatPlayers = DefaultMaxChatPlayers)
|
|
{
|
|
DisconnectChat(); // Leave any currently connected chat lobby
|
|
|
|
var connectArgs = new ChatConnectArgs(chatLobbyName, maxChatPlayers);
|
|
|
|
lobbyInterface.CreateLobbySearch(new CreateLobbySearchOptions { MaxResults = 1 }, out var searchHandle);
|
|
searchHandle.SetLobbyId(new LobbySearchSetLobbyIdOptions { LobbyId = chatLobbyName });
|
|
|
|
var localUserId = localUserProvider.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;
|
|
UpdateRTCRoomName();
|
|
Debug.Log($"Chat lobby creation successful, lobby ID = {connectedLobbyId}, RTC room name = {rtcRoomName}");
|
|
|
|
chatUsers.Clear();
|
|
SubscribeToRoomNotifications();
|
|
|
|
OnChatConnected?.Invoke();
|
|
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;
|
|
rtcRoomName = null;
|
|
Debug.Log("Chat lobby already exists, attempting to join it...");
|
|
ConnectToChat(connectArgs.chatLobbyName, connectArgs.maxChatPlayers);
|
|
break;
|
|
default:
|
|
Debug.LogError($"Chat lobby creation failed, result code = {data.ResultCode}");
|
|
connectedLobbyId = null;
|
|
rtcRoomName = null;
|
|
OnChatConnectionFailed?.Invoke();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void HandleLobbyJoined(JoinLobbyCallbackInfo data)
|
|
{
|
|
var connectArgs = (ChatConnectArgs)data.ClientData;
|
|
switch (data.ResultCode)
|
|
{
|
|
case Result.Success:
|
|
connectedLobbyId = data.LobbyId;
|
|
UpdateRTCRoomName();
|
|
Debug.Log($"Chat lobby joined successfully, lobby ID = {connectedLobbyId}, RTC room name = {rtcRoomName}");
|
|
|
|
chatUsers.Clear();
|
|
SubscribeToRoomNotifications();
|
|
|
|
OnChatConnected?.Invoke();
|
|
break;
|
|
default:
|
|
Debug.LogError($"Chat lobby join failed, result code = {data.ResultCode}");
|
|
connectedLobbyId = null;
|
|
rtcRoomName = null;
|
|
OnChatConnectionFailed?.Invoke();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void UpdateRTCRoomName()
|
|
{
|
|
rtcRoomName = null;
|
|
|
|
var result = lobbyInterface.GetRTCRoomName(
|
|
new GetRTCRoomNameOptions { LocalUserId = localUserProvider.Invoke(), LobbyId = connectedLobbyId },
|
|
out string roomName);
|
|
|
|
if (result != Result.Success)
|
|
{
|
|
Debug.LogError($"Failed to obtain RTC room name for lobby with ID {connectedLobbyId}");
|
|
return;
|
|
}
|
|
|
|
rtcRoomName = roomName;
|
|
}
|
|
|
|
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 = localUserProvider.Invoke(),
|
|
LobbyId = connectedLobbyId,
|
|
},
|
|
null,
|
|
data => { }
|
|
);
|
|
|
|
connectedLobbyId = null;
|
|
rtcRoomName = null;
|
|
chatUsers.Clear();
|
|
|
|
OnChatDisconnected?.Invoke();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mute or unmute the local player's voice chat. This can be used to implement push-to-talk.
|
|
/// </summary>
|
|
public void SetLocalMuted(bool muted)
|
|
{
|
|
if (!IsConnected)
|
|
return;
|
|
|
|
audioInterface.UpdateSending(new UpdateSendingOptions
|
|
{
|
|
LocalUserId = localUserProvider.Invoke(),
|
|
RoomName = rtcRoomName,
|
|
AudioStatus = muted ? RTCAudioStatus.Disabled : RTCAudioStatus.Enabled,
|
|
}, null, data => { });
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public void SetRemoteMuted(ProductUserId remoteUser, bool muted)
|
|
{
|
|
if (!IsConnected)
|
|
return;
|
|
|
|
audioInterface.UpdateReceiving(new UpdateReceivingOptions
|
|
{
|
|
LocalUserId = localUserProvider.Invoke(),
|
|
ParticipantId = remoteUser,
|
|
RoomName = rtcRoomName,
|
|
AudioEnabled = !muted,
|
|
}, null, data => { });
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public void MuteAllRemote()
|
|
{
|
|
if (!IsConnected)
|
|
return;
|
|
|
|
var localUserId = localUserProvider.Invoke();
|
|
foreach (var remoteProductUser in chatUsers.Keys)
|
|
{
|
|
audioInterface.UpdateReceiving(new UpdateReceivingOptions
|
|
{
|
|
LocalUserId = localUserId,
|
|
ParticipantId = remoteProductUser,
|
|
RoomName = rtcRoomName,
|
|
AudioEnabled = false,
|
|
}, null, data => { });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public void SetOutputVolume(float volume)
|
|
{
|
|
audioInterface.SetAudioOutputSettings(new SetAudioOutputSettingsOptions
|
|
{
|
|
LocalUserId = localUserProvider.Invoke(),
|
|
DeviceId = null, // Default output device
|
|
Volume = volume * 50f,
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the requested user is currently talking or not.
|
|
/// This can be either the local player or a remote user.
|
|
/// </summary>
|
|
public bool IsUserSpeaking(ProductUserId user)
|
|
{
|
|
if (chatUsers.TryGetValue(user, out var chatUser))
|
|
{
|
|
return chatUser.audioStatus == RTCAudioStatus.Enabled && chatUser.isSpeaking;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void SubscribeToAudioDeviceNotifications()
|
|
{
|
|
if (!onAudioDevicesChangedCallbackId.HasValue)
|
|
{
|
|
onAudioDevicesChangedCallbackId = audioInterface.AddNotifyAudioDevicesChanged(
|
|
new AddNotifyAudioDevicesChangedOptions(), null,
|
|
HandleAudioDevicesChanged);
|
|
|
|
// Call the event handler once to query the audio devices in their initial state
|
|
HandleAudioDevicesChanged(null);
|
|
}
|
|
}
|
|
|
|
private void UnsubscribeFromAudioDeviceNotifications()
|
|
{
|
|
if (onAudioDevicesChangedCallbackId.HasValue)
|
|
{
|
|
audioInterface.RemoveNotifyAudioDevicesChanged(onAudioDevicesChangedCallbackId.Value);
|
|
onAudioDevicesChangedCallbackId = null;
|
|
}
|
|
}
|
|
|
|
private void HandleAudioDevicesChanged(AudioDevicesChangedCallbackInfo data)
|
|
{
|
|
defaultInputDeviceId = null;
|
|
|
|
// Update the default input device, so we know whether we can actually talk or not
|
|
uint inputDevicesCount = audioInterface.GetAudioInputDevicesCount(new GetAudioInputDevicesCountOptions());
|
|
Debug.Log($"Found {inputDevicesCount} input device(s)");
|
|
|
|
for (uint inputDeviceIndex = 0; inputDeviceIndex < inputDevicesCount; ++inputDeviceIndex)
|
|
{
|
|
var inputDeviceInfo = audioInterface.GetAudioInputDeviceByIndex(
|
|
new GetAudioInputDeviceByIndexOptions { DeviceInfoIndex = inputDeviceIndex });
|
|
|
|
Debug.Log($"Input device {inputDeviceIndex}: ID = {inputDeviceInfo.DeviceId}, Name = {inputDeviceInfo.DeviceName}, Default = {inputDeviceInfo.DefaultDevice}");
|
|
|
|
if (inputDeviceInfo.DefaultDevice)
|
|
{
|
|
defaultInputDeviceId = inputDeviceInfo.DeviceId;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SubscribeToRoomNotifications()
|
|
{
|
|
var localUserId = localUserProvider.Invoke();
|
|
|
|
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 = rtcRoomName,
|
|
},
|
|
null,
|
|
HandleParticipantStatusChanged
|
|
);
|
|
}
|
|
|
|
if (!onParticipantUpdatedCallbackId.HasValue)
|
|
{
|
|
onParticipantUpdatedCallbackId = audioInterface.AddNotifyParticipantUpdated
|
|
(
|
|
new AddNotifyParticipantUpdatedOptions
|
|
{
|
|
LocalUserId = localUserId,
|
|
RoomName = rtcRoomName,
|
|
},
|
|
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)
|
|
{
|
|
// Note: reconnecting is handled automatically by the RTC lobby system
|
|
if (!data.IsConnected &&
|
|
(data.DisconnectReason == Result.UserKicked || data.DisconnectReason == Result.UserBanned))
|
|
{
|
|
// If we're either kicked or banned then we won't automatically reconnect, so leave the lobby properly
|
|
DisconnectChat();
|
|
}
|
|
}
|
|
|
|
private void HandleParticipantStatusChanged(ParticipantStatusChangedCallbackInfo data)
|
|
{
|
|
switch (data.ParticipantStatus)
|
|
{
|
|
case RTCParticipantStatus.Joined:
|
|
if (!chatUsers.ContainsKey(data.ParticipantId))
|
|
{
|
|
var chatUser = new ChatUser(data.ParticipantId);
|
|
chatUsers.Add(data.ParticipantId, chatUser);
|
|
OnChatUserJoined?.Invoke(data.ParticipantId);
|
|
}
|
|
break;
|
|
case RTCParticipantStatus.Left:
|
|
chatUsers.Remove(data.ParticipantId);
|
|
OnChatUserLeft?.Invoke(data.ParticipantId);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void HandleParticipantUpdated(ParticipantUpdatedCallbackInfo data)
|
|
{
|
|
if (chatUsers.TryGetValue(data.ParticipantId, out var chatUser))
|
|
{
|
|
chatUser.audioStatus = data.AudioStatus;
|
|
chatUser.isSpeaking = data.Speaking;
|
|
}
|
|
}
|
|
|
|
private class ChatConnectArgs
|
|
{
|
|
public readonly string chatLobbyName;
|
|
public readonly uint maxChatPlayers;
|
|
|
|
public ChatConnectArgs(string chatLobbyName, uint maxChatPlayers)
|
|
{
|
|
this.chatLobbyName = chatLobbyName;
|
|
this.maxChatPlayers = maxChatPlayers;
|
|
}
|
|
}
|
|
|
|
private class ChatUser
|
|
{
|
|
public readonly ProductUserId productUserId;
|
|
|
|
public RTCAudioStatus audioStatus;
|
|
public bool isSpeaking;
|
|
|
|
public ChatUser(ProductUserId productUserId)
|
|
{
|
|
this.productUserId = productUserId;
|
|
}
|
|
}
|
|
}
|