Browse Source

- Added product user mapper interface for converting between ProductUser and platform-specific player IDs

- Used the above to implement common mute and querying methods
- Handle kick and ban events
- Implemented player audio and speaking status updates
- Reimplemented local mute by disabling sending of audio
master
Nico de Poel 5 years ago
parent
commit
f017e2dcdf
  1. 149
      Assets/Scripts/EOSVoiceChat.cs
  2. 13
      Assets/Scripts/IProductUserMapper.cs
  3. 3
      Assets/Scripts/IProductUserMapper.cs.meta
  4. 13
      Assets/Scripts/MagnificentVoiceChat.cs

149
Assets/Scripts/EOSVoiceChat.cs

@ -20,24 +20,29 @@ public class EOSVoiceChat
private readonly RTCInterface rtcInterface;
private readonly RTCAudioInterface audioInterface;
private readonly Func<ProductUserId> productUserProvider;
private readonly IProductUserMapper productUserMapper;
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 ulong? onConnectionChangedCallbackId, onParticipantStatusChangedCallbackId, onParticipantUpdatedCallbackId;
/// <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> productUserProvider)
Func<ProductUserId> productUserProvider, IProductUserMapper productUserMapper)
{
this.lobbyInterface = lobbyInterface;
this.rtcInterface = rtcInterface;
this.audioInterface = audioInterface;
this.productUserProvider = productUserProvider;
this.productUserMapper = productUserMapper;
}
/// <summary>
@ -105,19 +110,24 @@ public class EOSVoiceChat
{
case Result.Success:
connectedLobbyId = data.LobbyId;
Debug.Log($"Chat lobby creation successful, lobby ID = {connectedLobbyId}");
connectArgs.onCompleted?.Invoke(true);
UpdateRTCRoomName();
Debug.Log($"Chat lobby creation successful, lobby ID = {connectedLobbyId}, RTC room name = {rtcRoomName}");
chatUsers.Clear();
SubscribeToRoomNotifications();
connectArgs.onCompleted?.Invoke(true);
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.onCompleted, connectArgs.maxChatPlayers);
break;
default:
connectedLobbyId = null;
rtcRoomName = null;
Debug.LogError($"Chat lobby creation failed, result code = {data.ResultCode}");
connectArgs.onCompleted?.Invoke(false);
break;
@ -131,18 +141,39 @@ public class EOSVoiceChat
{
case Result.Success:
connectedLobbyId = data.LobbyId;
Debug.Log($"Chat lobby joined successfully, lobby ID = {connectedLobbyId}");
connectArgs.onCompleted?.Invoke(true);
UpdateRTCRoomName();
Debug.Log($"Chat lobby joined successfully, lobby ID = {connectedLobbyId}, RTC room name = {rtcRoomName}");
chatUsers.Clear();
SubscribeToRoomNotifications();
connectArgs.onCompleted?.Invoke(true);
break;
default:
connectedLobbyId = null;
rtcRoomName = null;
Debug.LogError($"Chat lobby join failed, result code = {data.ResultCode}");
connectArgs.onCompleted?.Invoke(false);
break;
}
}
private void UpdateRTCRoomName()
{
rtcRoomName = null;
var result = lobbyInterface.GetRTCRoomName(
new GetRTCRoomNameOptions { LocalUserId = productUserProvider.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)
@ -163,6 +194,8 @@ public class EOSVoiceChat
);
connectedLobbyId = null;
rtcRoomName = null;
chatUsers.Clear();
}
/// <summary>
@ -173,25 +206,33 @@ public class EOSVoiceChat
if (!IsConnected)
return;
// TODO: can also disable Sending of data entirely
audioInterface.SetAudioInputSettings(new SetAudioInputSettingsOptions
audioInterface.UpdateSending(new UpdateSendingOptions
{
LocalUserId = productUserProvider.Invoke(),
DeviceId = null, // Default input device
Volume = muted ? 0f : 100f,
});
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(string playerId, bool muted)
public void SetRemoteMuted(string remotePlayerId, bool muted)
{
if (!IsConnected)
return;
productUserMapper.MapPlatformIdToProductUser(remotePlayerId, remoteProductUser =>
{
audioInterface.UpdateReceiving(new UpdateReceivingOptions
{
LocalUserId = productUserProvider.Invoke(),
ParticipantId = remoteProductUser,
RoomName = rtcRoomName,
AudioEnabled = !muted,
}, null, data => { });
});
}
/// <summary>
@ -202,7 +243,18 @@ public class EOSVoiceChat
{
if (!IsConnected)
return;
var localUserId = productUserProvider.Invoke();
foreach (var remoteProductUser in chatUsers.Keys)
{
audioInterface.UpdateReceiving(new UpdateReceivingOptions
{
LocalUserId = localUserId,
ParticipantId = remoteProductUser,
RoomName = rtcRoomName,
AudioEnabled = false,
}, null, data => { });
}
}
/// <summary>
@ -218,23 +270,30 @@ public class EOSVoiceChat
Volume = volume * 50f,
});
}
/// <summary>
/// Whether the requested player is currently talking or not.
/// This can be either the local player or a remote player.
/// </summary>
public bool IsUserTalking(string playerId)
{
foreach (var chatUser in chatUsers.Values)
{
if (chatUser.platformPlayerId != playerId)
continue;
return chatUser.audioStatus == RTCAudioStatus.Enabled && chatUser.isSpeaking;
}
return false;
}
// 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
@ -256,7 +315,7 @@ public class EOSVoiceChat
new AddNotifyParticipantStatusChangedOptions
{
LocalUserId = localUserId,
RoomName = roomName,
RoomName = rtcRoomName,
},
null,
HandleParticipantStatusChanged
@ -270,7 +329,7 @@ public class EOSVoiceChat
new AddNotifyParticipantUpdatedOptions
{
LocalUserId = localUserId,
RoomName = roomName,
RoomName = rtcRoomName,
},
null,
HandleParticipantUpdated
@ -303,16 +362,44 @@ public class EOSVoiceChat
{
Debug.Log($"RTC Room connection changed, connected = {data.IsConnected}, disconnect reason = {data.DisconnectReason}");
// Note: reconnecting is handled automatically by the 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)
{
Debug.Log($"Participant status changed, participant = {data.ParticipantId}, status = {data.ParticipantStatus}");
switch (data.ParticipantStatus)
{
case RTCParticipantStatus.Joined:
if (chatUsers.ContainsKey(data.ParticipantId))
return;
var chatUser = new ChatUser(data.ParticipantId);
productUserMapper.MapProductUserToPlatformId(data.ParticipantId, pid => chatUser.platformPlayerId = pid);
chatUsers.Add(data.ParticipantId, chatUser);
break;
case RTCParticipantStatus.Left:
chatUsers.Remove(data.ParticipantId);
break;
}
}
private void HandleParticipantUpdated(ParticipantUpdatedCallbackInfo data)
{
Debug.Log($"Participant status changed, participant = {data.ParticipantId}, speaking = {data.Speaking}, audio status = {data.AudioStatus}");
if (chatUsers.TryGetValue(data.ParticipantId, out var chatUser))
{
chatUser.audioStatus = data.AudioStatus;
chatUser.isSpeaking = data.Speaking;
}
}
private class ChatConnectArgs
@ -332,8 +419,14 @@ public class EOSVoiceChat
private class ChatUser
{
public readonly ProductUserId productUserId;
public string platformPlayerId;
public string platformPlayerId;
public RTCAudioStatus audioStatus;
public bool isSpeaking;
public ChatUser(ProductUserId productUserId)
{
this.productUserId = productUserId;
}
}
}

13
Assets/Scripts/IProductUserMapper.cs

@ -0,0 +1,13 @@
using System;
using Epic.OnlineServices;
/// <summary>
/// Generic interface for converting between EOS Product Users and platform-specific Player IDs.
/// The exact rules for mapping from one to the other may differ per platform, e.g. Steam and Epic will use different logic than Xbox and PlayStation.
/// The methods in this interface are asynchronous, though implementations may choose to pre-cache mappings to speed up the process.
/// </summary>
public interface IProductUserMapper
{
void MapProductUserToPlatformId(ProductUserId productUserId, Action<string> platformIdCallback);
void MapPlatformIdToProductUser(string platformId, Action<ProductUserId> productUserCallback);
}

3
Assets/Scripts/IProductUserMapper.cs.meta

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d8a80c90c491474dbe97eba62578254f
timeCreated: 1625665094

13
Assets/Scripts/MagnificentVoiceChat.cs

@ -126,7 +126,7 @@ public class MagnificentVoiceChat : MonoBehaviour
ScopeFlags = AuthScopeFlags.BasicProfile,
}, null, HandleLoginResult);
voiceChat = new EOSVoiceChat(lobbyInterface, rtcInterface, audioInterface, () => localProductUserId);
voiceChat = new EOSVoiceChat(lobbyInterface, rtcInterface, audioInterface, () => localProductUserId, new NilProductUserMapper());
}
private Credentials GetEpicCredentials() // This is platform-specific
@ -305,4 +305,15 @@ public class MagnificentVoiceChat : MonoBehaviour
GUILayout.Label(status.ToString());
}
private class NilProductUserMapper : IProductUserMapper
{
public void MapProductUserToPlatformId(ProductUserId productUserId, Action<string> platformIdCallback)
{
}
public void MapPlatformIdToProductUser(string platformId, Action<ProductUserId> productUserCallback)
{
}
}
}
Loading…
Cancel
Save