@ -20,24 +20,29 @@ public class EOSVoiceChat
private readonly RTCInterface rtcInterface ;
private readonly RTCInterface rtcInterface ;
private readonly RTCAudioInterface audioInterface ;
private readonly RTCAudioInterface audioInterface ;
private readonly Func < ProductUserId > productUserProvider ;
private readonly Func < ProductUserId > productUserProvider ;
private readonly IProductUserMapper productUserMapper ;
private readonly Dictionary < ProductUserId , ChatUser > chatUsers = new Dictionary < ProductUserId , ChatUser > ( ) ;
private string connectedLobbyId ;
private string connectedLobbyId ;
private string rtcRoomName ;
public bool IsConnected = > lobbyInterface ! = null & & rtcInterface ! = null & & audioInterface ! = null & & ! string . IsNullOrEmpty ( connectedLobbyId ) ;
public bool IsConnected = > lobbyInterface ! = null & & rtcInterface ! = null & & audioInterface ! = null & & ! string . IsNullOrEmpty ( connectedLobbyId ) ;
private ulong? onConnectionChangedCallbackId , onParticipantStatusChangedCallbackId , onParticipantUpdatedCallbackId ;
private ulong? onConnectionChangedCallbackId , onParticipantStatusChangedCallbackId , onParticipantUpdatedCallbackId ;
/// <summary>
/// <summary>
/// Provide the required interfaces for voice chat. Product User ID is provided through a callback, so that the
/// 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.
/// same instance of this class can remain in use even if the logged in user changes.
/// </summary>
/// </summary>
public EOSVoiceChat (
public EOSVoiceChat (
LobbyInterface lobbyInterface , RTCInterface rtcInterface , RTCAudioInterface audioInterface ,
LobbyInterface lobbyInterface , RTCInterface rtcInterface , RTCAudioInterface audioInterface ,
Func < ProductUserId > productUserProvider )
Func < ProductUserId > productUserProvider , IProductUserMapper productUserMapper )
{
{
this . lobbyInterface = lobbyInterface ;
this . lobbyInterface = lobbyInterface ;
this . rtcInterface = rtcInterface ;
this . rtcInterface = rtcInterface ;
this . audioInterface = audioInterface ;
this . audioInterface = audioInterface ;
this . productUserProvider = productUserProvider ;
this . productUserProvider = productUserProvider ;
this . productUserMapper = productUserMapper ;
}
}
/// <summary>
/// <summary>
@ -105,19 +110,24 @@ public class EOSVoiceChat
{
{
case Result . Success :
case Result . Success :
connectedLobbyId = data . LobbyId ;
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 ( ) ;
SubscribeToRoomNotifications ( ) ;
connectArgs . onCompleted ? . Invoke ( true ) ;
break ;
break ;
case Result . LobbyLobbyAlreadyExists :
case Result . LobbyLobbyAlreadyExists :
// This can happen if two clients try to create the same lobby at the same time, a classic race condition.
// 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.
// Try to join the other client's newly created chat lobby instead.
connectedLobbyId = null ;
connectedLobbyId = null ;
rtcRoomName = null ;
Debug . Log ( "Chat lobby already exists, attempting to join it..." ) ;
Debug . Log ( "Chat lobby already exists, attempting to join it..." ) ;
ConnectToChat ( connectArgs . chatLobbyName , connectArgs . onCompleted , connectArgs . maxChatPlayers ) ;
ConnectToChat ( connectArgs . chatLobbyName , connectArgs . onCompleted , connectArgs . maxChatPlayers ) ;
break ;
break ;
default :
default :
connectedLobbyId = null ;
connectedLobbyId = null ;
rtcRoomName = null ;
Debug . LogError ( $"Chat lobby creation failed, result code = {data.ResultCode}" ) ;
Debug . LogError ( $"Chat lobby creation failed, result code = {data.ResultCode}" ) ;
connectArgs . onCompleted ? . Invoke ( false ) ;
connectArgs . onCompleted ? . Invoke ( false ) ;
break ;
break ;
@ -131,18 +141,39 @@ public class EOSVoiceChat
{
{
case Result . Success :
case Result . Success :
connectedLobbyId = data . LobbyId ;
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 ( ) ;
SubscribeToRoomNotifications ( ) ;
connectArgs . onCompleted ? . Invoke ( true ) ;
break ;
break ;
default :
default :
connectedLobbyId = null ;
connectedLobbyId = null ;
rtcRoomName = null ;
Debug . LogError ( $"Chat lobby join failed, result code = {data.ResultCode}" ) ;
Debug . LogError ( $"Chat lobby join failed, result code = {data.ResultCode}" ) ;
connectArgs . onCompleted ? . Invoke ( false ) ;
connectArgs . onCompleted ? . Invoke ( false ) ;
break ;
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 ( )
public void DisconnectChat ( )
{
{
if ( ! IsConnected )
if ( ! IsConnected )
@ -163,6 +194,8 @@ public class EOSVoiceChat
) ;
) ;
connectedLobbyId = null ;
connectedLobbyId = null ;
rtcRoomName = null ;
chatUsers . Clear ( ) ;
}
}
/// <summary>
/// <summary>
@ -173,25 +206,33 @@ public class EOSVoiceChat
if ( ! IsConnected )
if ( ! IsConnected )
return ;
return ;
// TODO: can also disable Sending of data entirely
audioInterface . SetAudioInputSettings ( new SetAudioInputSettingsOptions
audioInterface . UpdateSending ( new UpdateSendingOptions
{
{
LocalUserId = productUserProvider . Invoke ( ) ,
LocalUserId = productUserProvider . Invoke ( ) ,
DeviceId = null , // Default input device
Volume = muted ? 0f : 1 0 0f ,
} ) ;
RoomName = rtcRoomName ,
AudioStatus = muted ? RTCAudioStatus . Disabled : RTCAudioStatus . Enabled ,
} , null , data = > { } ) ;
}
}
/// <summary>
/// <summary>
/// Mute or unmute a specific remove player. This can be used to filter out specific players in the chat lobby,
/// 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.
/// or for manually muting toxic players.
/// </summary>
/// </summary>
public void SetRemoteMuted ( string p layerId, bool muted )
public void SetRemoteMuted ( string remoteP layerId, bool muted )
{
{
if ( ! IsConnected )
if ( ! IsConnected )
return ;
return ;
productUserMapper . MapPlatformIdToProductUser ( remotePlayerId , remoteProductUser = >
{
audioInterface . UpdateReceiving ( new UpdateReceivingOptions
{
LocalUserId = productUserProvider . Invoke ( ) ,
ParticipantId = remoteProductUser ,
RoomName = rtcRoomName ,
AudioEnabled = ! muted ,
} , null , data = > { } ) ;
} ) ;
}
}
/// <summary>
/// <summary>
@ -202,7 +243,18 @@ public class EOSVoiceChat
{
{
if ( ! IsConnected )
if ( ! IsConnected )
return ;
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>
/// <summary>
@ -218,23 +270,30 @@ public class EOSVoiceChat
Volume = volume * 5 0f ,
Volume = volume * 5 0f ,
} ) ;
} ) ;
}
}
/// <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 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 ( )
private void SubscribeToRoomNotifications ( )
{
{
var localUserId = productUserProvider . Invoke ( ) ;
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 )
if ( ! onConnectionChangedCallbackId . HasValue )
{
{
onConnectionChangedCallbackId = lobbyInterface . AddNotifyRTCRoomConnectionChanged
onConnectionChangedCallbackId = lobbyInterface . AddNotifyRTCRoomConnectionChanged
@ -256,7 +315,7 @@ public class EOSVoiceChat
new AddNotifyParticipantStatusChangedOptions
new AddNotifyParticipantStatusChangedOptions
{
{
LocalUserId = localUserId ,
LocalUserId = localUserId ,
RoomName = roomName ,
RoomName = rtcR oomName ,
} ,
} ,
null ,
null ,
HandleParticipantStatusChanged
HandleParticipantStatusChanged
@ -270,7 +329,7 @@ public class EOSVoiceChat
new AddNotifyParticipantUpdatedOptions
new AddNotifyParticipantUpdatedOptions
{
{
LocalUserId = localUserId ,
LocalUserId = localUserId ,
RoomName = roomName ,
RoomName = rtcR oomName ,
} ,
} ,
null ,
null ,
HandleParticipantUpdated
HandleParticipantUpdated
@ -303,16 +362,44 @@ public class EOSVoiceChat
{
{
Debug . Log ( $"RTC Room connection changed, connected = {data.IsConnected}, disconnect reason = {data.DisconnectReason}" ) ;
Debug . Log ( $"RTC Room connection changed, connected = {data.IsConnected}, disconnect reason = {data.DisconnectReason}" ) ;
// Note: reconnecting is handled automatically by the lobby system
// 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 )
private void HandleParticipantStatusChanged ( ParticipantStatusChangedCallbackInfo data )
{
{
Debug . Log ( $"Participant status changed, participant = {data.ParticipantId}, status = {data.ParticipantStatus}" ) ;
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 )
private void HandleParticipantUpdated ( ParticipantUpdatedCallbackInfo data )
{
{
Debug . Log ( $"Participant status changed, participant = {data.ParticipantId}, speaking = {data.Speaking}, audio status = {data.AudioStatus}" ) ;
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
private class ChatConnectArgs
@ -332,8 +419,14 @@ public class EOSVoiceChat
private class ChatUser
private class ChatUser
{
{
public readonly ProductUserId productUserId ;
public readonly ProductUserId productUserId ;
public string platformPlayerId ;
public string platformPlayerId ;
public RTCAudioStatus audioStatus ;
public bool isSpeaking ;
public ChatUser ( ProductUserId productUserId )
{
this . productUserId = productUserId ;
}
}
}
}
}