Note: I am using the UE4 source from the release branch of UE4.15.0
Using OnlineSubsystemNull, with voice chat properly enabled, and if a Listen Server has no microphone, and if a connecting Client does have a microphone, and the connecting Client’s voice is enabled and broadcasting, and if the Listen Server receives any voice data from the connecting Client, the Listen Server will kick the connecting Client immediately. Debugging the engine’s source code, the reason this happens is slightly convoluted, as follows…
The problem begins when FOnlineSubsystemNull::GetVoiceInterface()
attempts to Init()
its FOnlineVoiceImpl
implementing IOnlineVoice
on the Listen Server:
IOnlineVoicePtr FOnlineSubsystemNull::GetVoiceInterface() const
{
if (VoiceInterface.IsValid() && !bVoiceInterfaceInitialized)
{
if (!VoiceInterface->Init())
{
VoiceInterface = nullptr;
}
bVoiceInterfaceInitialized = true;
}
return VoiceInterface;
}
Looking at FOnlineVoiceImpl::Init()
, we see:
bool FOnlineVoiceImpl::Init()
{
bool bSuccess = false;
// irrelevant code...
bool bHasVoiceEnabled = false;
if (GConfig->GetBool(TEXT("OnlineSubsystem"), TEXT("bHasVoiceEnabled"), bHasVoiceEnabled, GEngineIni) && bHasVoiceEnabled)
{
// irrelevant code...
if (bSuccess)
{
const bool bVoiceEngineForceDisable = OnlineSubsystem->IsDedicated() || GIsBuildMachine;
if (!bVoiceEngineForceDisable)
{
VoiceEngine = MakeShareable(new FVoiceEngineImpl(OnlineSubsystem));
bSuccess = VoiceEngine->Init(MaxLocalTalkers, MaxRemoteTalkers);
}
// irrelevant code...
}
// irrelevant code...
}
// irrelevant code...
return bSuccess;
}
Which calls bSuccess = VoiceEngine->Init(MaxLocalTalkers, MaxRemoteTalkers);
, so looking at FVoiceEngineImpl::Init()
, we see:
bool FVoiceEngineImpl::Init(int32 MaxLocalTalkers, int32 MaxRemoteTalkers)
{
bool bSuccess = false;
if (!OnlineSubsystem->IsDedicated())
{
FVoiceModule& VoiceModule = FVoiceModule::Get();
if (VoiceModule.IsVoiceEnabled())
{
VoiceCapture = VoiceModule.CreateVoiceCapture();
VoiceEncoder = VoiceModule.CreateVoiceEncoder();
bSuccess = VoiceCapture.IsValid() && VoiceEncoder.IsValid();
// irrelevant code...
}
// irrelevant code...
}
return bSuccess;
}
Which is where we start to see potential problems, namely with VoiceCapture = VoiceModule.CreateVoiceCapture();
. I could continue to go deeper into the code, but suffice it to say that because a microphone doesn’t exist, bSuccess
returns as false
, and the FOnlineSubsystemNull::VoiceInterface->Init();
fails, which causes the entire FOnlineSubsystemNull::VoiceInterface
to remain nullptr
throughout the rest of the program’s execution. This looks like it should be fine, after all, code exists to account for a nullptr FOnlineSubsystemNull::VoiceInterface
, but the real problem arises later on, when our first microphone-enabled Client joins our game: this Client’s FOnlineSubsystemNull::VoiceInterface
is not nullptr
because their microphone does exist, and therefore, they begin to send voice packets to the Listen Server, and their serialization is at some point delegated to UVoiceChannel::ReceivedBunch()
:
void UVoiceChannel::ReceivedBunch(FInBunch& Bunch)
{
if (Connection->Driver && Connection->Driver->World)
{
while (!Bunch.AtEnd())
{
// Give the data to the local voice processing
TSharedPtr<FVoicePacket> VoicePacket = UOnlineEngineInterface::Get()->SerializeRemotePacket(Connection->Driver->World, Bunch);
if (VoicePacket.IsValid())
{
// irrelevant code...
}
else
{
// Unable to serialize the data because the serializer doesn't exist or there was a problem with this packet
Bunch.SetError();
break;
}
}
}
}
And, again, looking at UOnlineEngineInterfaceImpl::SerializeRemotePacket()
:
TSharedPtr<FVoicePacket> UOnlineEngineInterfaceImpl::SerializeRemotePacket(UWorld* World, FArchive& Ar)
{
IOnlineVoicePtr VoiceInt = Online::GetVoiceInterface(World);
if (VoiceInt.IsValid())
{
return VoiceInt->SerializeRemotePacket(Ar);
}
return nullptr;
}
We see that it will return an invalid VoicePacket
, because of the null FOnlineSubsystemNull::VoiceInterface
, causing Bunch.SetError();
to be called. We now travel up the callstack to UChannel::ReceivedRawBunch()
where we encounter…
UE_LOG( LogNetTraffic, Error, TEXT( "UChannel::ReceivedRawBunch: Bunch.IsError() after ReceivedNextBunch 1" ) );
…which finally causes UNetConnection::ReceivedPacket()
to log the following error and disconnect the client for fear of a server crash attack.
UE_LOG( LogNetTraffic, Error, TEXT("Received corrupted packet data from client %s. Disconnecting."), *LowLevelGetRemoteAddress() );
Given all of this diagnostic info, it looks as though there is a structural weakness in the design of the voice system for the given case. A recommended fix that would not break the current design at a high level, or create messy code would be to support the FVoiceEngineImpl
existing without a VoiceCapture
, and just passing through silence should a steam of voice data be necessary.