Skip to main content

Voice chat on dedicated servers

If you want to provide voice chat for players on a dedicated server, you'll need to make the dedicated server issue channel credentials for connecting players, send the issued credentials over Unreal Engine networking, and then have the clients join the channel using the issued credentials.

You can't use voice-enabled lobbies on dedicated servers, because using EOS lobbies requires a signed in user, which dedicated servers don't have.

If you're using C++, you'll need to add the Online Interfaces headers to your project so you can access the voice admin APIs.

Choosing a channel name for the voice chat room

When a player connects to the server, you'll need to issue credentials for a named voice channel. Since dedicated servers have administrative voice privileges, you can use any name you like for voice channels. You don't need to explicitly "create" voice channels; just issuing credentials for a name of your choice is enough.

If you want all players that are connected to a dedicated server to be in the same voice chat, you can use the session ID as a channel name, since it will be unique to that server.

However, you're not limited to this. For example, you could use a channel name that is unique to each team of players, which would give each team their own voice chat (without relying on the team all being in the same party before joining the server).

Issue the channel credentials on player connect, and connect to the channel on the client

When clients connect to the dedicated server, you'll need to issue channel credentials and then send them the issue credentials. They'll then connect to the voice channel with those credentials.

You'll need to ensure that your custom player controllers have an IVoiceChatUser instance which is already connected with the locally signed in user on the client. You can refer to Voice chat overview on how to create a voice chat user and connect it to the EOS online subsystem.

In your custom player controller class, declare a function that runs on the owning clients:

#include "OnlineSubsystem.h"
#include "OnlineSubsystemUtils.h"
#include "VoiceChat.h"

// ...

class ACustomPlayerController : public APlayerController
{
// ...

private:
// Refer to "Voice chat overview" on how to create and destroy this instance.
IVoiceChatUser* VoiceChatUser;

public:
UFUNCTION(Client, Reliable)
void JoinVoiceChannel(const FString& InChannelName, const FString& InChannelCredentials);

// ...
};

Implement the JoinVoiceChannel function to make it join the voice channel on the client:

void ACustomPlayerController::JoinVoiceChannel_Implementation(const FString& InChannelName, const FString& InChannelCredentials)
{
if (this->VoiceChatUser == nullptr)
{
return;
}

this->VoiceChatUser->JoinChannel(
InChannelName,
InChannelCredentials,
EVoiceChatChannelType::NonPositional,
FOnVoiceChatChannelJoinCompleteDelegate::CreateLambda([](const FString& ChannelName, const FVoiceChatResult& Result)
{
if (Result.IsSuccess())
{
// The user has joined the voice chat channel.
}
}));
}

In your custom game mode, add a function for handling the credential issuance response and override the PostLogin event:

class ACustomGameModeBase : public AGameModeBase
{
// ...

private:
void IssuedCredentialsForPlayer(
const FOnlineError& Result,
const FUniqueNetId& LocalUserId,
const TArray<FVoiceAdminChannelCredentials>& Credentials,
FString InChannelName,
ACustomPlayerController* NewPlayer);

public:
virtual void PostLogin(APlayerController* NewPlayer) override;

// ...
};

Handle the PostLogin event to issue channel credentials and call JoinVoiceChannel:

// If your project can't find this header, make sure you have installed the headers from here:
// https://src.redpoint.games/redpointgames/online-interfaces/
#include "OnlineVoiceAdminInterface.h"

// ...

void ACustomGameModeBase::IssuedCredentialsForPlayer(
const FOnlineError& Result,
const FUniqueNetId& LocalUserId,
const TArray<FVoiceAdminChannelCredentials>& Credentials,
FString InChannelName,
ACustomPlayerController* NewPlayer)
{
if (!Result.WasSuccessful())
{
return;
}

// We're making one CreateChannelCredentials call per player, so we can assume that the only entry in the
// results is for the player this callback is associated with. If you were requesting credentials for multiple
// target players, you'd have to iterate through Credentials to find the matching "TargetUserId" field.
if (IsValid(NewPlayer))
{
NewPlayer->JoinVoiceChannel(InChannelName, Credentials[0].ChannelCredentials);
}
}

void ACustomGameModeBase::PostLogin(APlayerController* NewPlayer)
{
Super::PostLogin(NewPlayer);

if (NewPlayer->IsLocalPlayerController())
{
return;
}

ACustomPlayerController* CustomPC = Cast<ACustomPlayerController>(NewPlayer);
if (!IsValid(CustomPC))
{
// Not the correct type of player controller.
return;
}
UNetConnection* IncomingNetConnection = Cast<UNetConnection>(CustomPC->Player);
if (!IsValid(IncomingNetConnection) || !IncomingNetConnection->PlayerId.IsValid())
{
// Not a networked player controller, or there's no player ID.
return;
}

IOnlineSubsystem* Subsystem = Online::GetSubsystem(this->GetWorld());
TSharedPtr<IOnlineIdentity, ESPMode::ThreadSafe> Identity = Subsystem->GetIdentityInterface();
TSharedPtr<IOnlineVoiceAdmin, ESPMode::ThreadSafe> VoiceAdmin = Online::GetVoiceAdminInterface(Subsystem);

// For the channel name you could use the current session ID. It's up to you
// to choose an appropriate value based on how you want to group players in
// voice chat.
FString ChannelName = TEXT("TODO");

TArray<TSharedRef<const FUniqueNetId>> TargetUserIds;
TargetUserIds.Add(IncomingNetConnection->PlayerId.GetUniqueNetId().ToSharedRef());
VoiceAdmin->CreateChannelCredentials(
*Identity->GetUniquePlayerId(0), // This will get the "dedicated server" ID.
ChannelName,
TargetUserIds,
FOnVoiceAdminCreateChannelCredentialsComplete::CreateUObject(
this,
&ACustomGameModeBase::IssuedCredentialsForPlayer,
ChannelName,
CustomPC));
}

Kicking voice chat participants on the server (e.g. when the player leaves the server)

When players disconnect from your dedicated game server, you'll want to remove them from voice chat. You can do so by calling the KickParticipant function on the voice admin API in response to the Logout event on the game mode.

First, override the PreLogout function on the game mode:

class ACustomGameModeBase : public AGameModeBase
{
// ...

public:
virtual void Logout(AController* Exiting) override;
};

Implement the Logout override and call KickParticipant:

void ACustomGameModeBase::Logout(AController* Exiting)
{
if (!IsValid(Exiting))
{
return;
}
APlayerController* PC = Cast<APlayerController>(Exiting);
if (!IsValid(PC))
{
return;
}
if (!PC->IsLocalPlayerController() || !IsValid(PC->Player))
{
return;
}
UNetConnection* PCNet = Cast<UNetConnection>(PC->Player);
if (!IsValid(PCNet) || !PCNet->PlayerId.IsValid())
{
return;
}

TSharedRef<const FUniqueNetId> UserId = PCNet->PlayerId.GetUniqueNetId().ToSharedRef();

IOnlineSubsystem* Subsystem = Online::GetSubsystem(this->GetWorld());
TSharedPtr<IOnlineIdentity, ESPMode::ThreadSafe> Identity = Subsystem->GetIdentityInterface();
TSharedPtr<IOnlineVoiceAdmin, ESPMode::ThreadSafe> VoiceAdmin = Online::GetVoiceAdminInterface(Subsystem);

// You'll need to use the same channel as when you issued credentials in the first place.
FString ChannelName = TEXT("TODO");

VoiceAdmin->KickParticipant(
*Identity->GetUniquePlayerId(0), // This will get the "dedicated server" ID.
ChannelName,
*UserId,
FOnVoiceAdminKickParticipantComplete::CreateLambda([](
const FOnlineError& Result,
const FUniqueNetId& LocalUserId,
const FUniqueNetId& TargetUserId)
{
if (!Result.WasSuccessful())
{
// User couldn't be kicked. They've already left the server at this point, so
// all you can really do is log the error.
}
}));

// This must be at the end, or the player's unique net ID will have been unset.
Super::Logout(Exiting);
}