Files
UnrealEngine/Engine/Plugins/Media/PixelStreaming2/Source/PixelStreaming2RTC/Private/Tests/TestUtils.cpp
2025-05-18 13:04:45 +08:00

436 lines
13 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "TestUtils.h"
#include "EpicRtcStreamer.h"
#include "EpicRtcVideoBufferI420.h"
#include "GenericPlatform/GenericPlatformTime.h"
#include "IPixelStreaming2Module.h"
#include "Logging.h"
#include "PixelCaptureInputFrameI420.h"
#include "PixelStreaming2PluginSettings.h"
#include "UtilsAsync.h"
#include "VideoProducer.h"
#include "SocketUtils.h"
#include "SocketSubsystem.h"
#if WITH_DEV_AUTOMATION_TESTS
namespace UE::PixelStreaming2
{
int32 TestUtils::NextStreamerPort()
{
// Start of IANA un-registerable ports (49152 - 65535)
static int NextStreamerPort = 49152;
NextStreamerPort = GetNextAvailablePort(NextStreamerPort);
return NextStreamerPort++;
}
int32 TestUtils::NextPlayerPort()
{
// Half of IANA un-registerable ports (49152 - 65535)
static int NextPlayerPort = 57344;
NextPlayerPort = GetNextAvailablePort(NextPlayerPort);
return NextPlayerPort++;
}
/* ---------- Latent Automation Commands ----------- */
bool FWaitSeconds::Update()
{
double DeltaTime = FPlatformTime::Seconds() - StartTime;
if (DeltaTime > WaitSeconds)
{
return true;
}
return false;
}
bool FSendSolidColorFrame::Update()
{
TSharedPtr<FPixelCaptureBufferI420> Buffer = MakeShared<FPixelCaptureBufferI420>(FrameConfig.Width, FrameConfig.Height);
uint8_t* yData = Buffer->GetMutableDataY();
uint8_t* uData = Buffer->GetMutableDataU();
uint8_t* vData = Buffer->GetMutableDataV();
for (int y = 0; y < Buffer->GetHeight(); ++y)
{
for (int x = 0; x < Buffer->GetWidth(); ++x)
{
const int x2 = x / 2;
const int y2 = y / 2;
yData[x + (y * Buffer->GetStrideY())] = FrameConfig.Y;
uData[x2 + (y2 * Buffer->GetStrideUV())] = FrameConfig.U;
vData[x2 + (y2 * Buffer->GetStrideUV())] = FrameConfig.V;
}
}
VideoProducer->PushFrame(FPixelCaptureInputFrameI420(Buffer));
return true;
}
bool FSendCustomMessageToStreamer::Update()
{
UE_LOG(LogPixelStreaming2RTC, Log, TEXT("FSendCustomMessageToStreamer: %s"), *MessageType);
if (Player->DataChannelAvailable())
{
if (!Player->SendMessage(MessageType, Body))
{
UE_LOG(LogPixelStreaming2RTC, Error, TEXT("Data channel send message failed."));
}
}
else
{
UE_LOG(LogPixelStreaming2RTC, Error, TEXT("No DataChannel on player."));
}
return true;
}
bool FSendDataChannelMessageToStreamer::Update()
{
UE_LOG(LogPixelStreaming2RTC, Log, TEXT("SendDataChannelMessageToStreamer: %s, %s"), *MessageType, *Body);
if (Player->DataChannelAvailable())
{
if (!Player->SendMessage(MessageType, Body))
{
UE_LOG(LogPixelStreaming2RTC, Error, TEXT("Data channel send message failed."));
}
}
else
{
UE_LOG(LogPixelStreaming2RTC, Error, TEXT("No DataChannel on player."));
}
return true;
}
bool FSendDataChannelMessageFromStreamer::Update()
{
UE_LOG(LogPixelStreaming2RTC, Log, TEXT("SendDataChannelMessageFromStreamer: %s, %s"), *MessageType, *Body);
if (Streamer)
{
Streamer->SendAllPlayersMessage(MessageType, Body);
}
else
{
UE_LOG(LogPixelStreaming2RTC, Error, TEXT("No DataChannel on player."));
}
return true;
}
bool FWaitForFrameReceived::Update()
{
if (VideoSink && VideoSink->HasReceivedFrame())
{
UE_LOG(LogPixelStreaming2RTC, Log, TEXT("Successfully received streamed frame."));
TRefCountPtr<EpicRtcVideoBufferInterface> Buffer = VideoSink->GetReceivedBuffer();
FString WidthTestString = FString::Printf(TEXT("Expected frame res=%dx%d, actual res=%dx%d"),
FrameConfig.Width,
FrameConfig.Height,
Buffer->GetWidth(),
Buffer->GetHeight());
if (FrameConfig.Width != Buffer->GetWidth() || FrameConfig.Height != Buffer->GetHeight())
{
UE_LOG(LogPixelStreaming2RTC, Error, TEXT("%s"), *WidthTestString);
}
else
{
UE_LOG(LogPixelStreaming2RTC, Log, TEXT("%s"), *WidthTestString);
}
EpicRtcPixelFormat Format = Buffer->GetFormat();
if (Format != EpicRtcPixelFormat::I420)
{
UE_LOG(LogPixelStreaming2RTC, Error, TEXT("Invalid Pixel Format"));
}
uint8_t* DataY = reinterpret_cast<uint8_t*>(Buffer->GetData());
uint8_t* DataU = DataY + Buffer->GetWidth() * Buffer->GetHeight();
uint8_t* DataV = DataU + ((Buffer->GetWidth() + 1) / 2) * ((Buffer->GetHeight() + 1) / 2);
/* ----- Test the pixels of the received frame ---- */
// Due this frame being a single solid color we "should" only need to look at a single element.
FString PixelTestString = FString::Printf(TEXT("Expected solid color frame.| Expect: Y=%d, Actual: Y=%d | Expected: U=%d, Actual: U=%d | Expected: V=%d, Actual: V=%d"),
FrameConfig.Y,
DataY[0],
FrameConfig.U,
DataU[0],
FrameConfig.V,
DataV[0]);
const int YDelta = FMath::Max(FrameConfig.Y, DataY[0]) - FMath::Min(FrameConfig.Y, DataY[0]);
const int UDelta = FMath::Max(FrameConfig.U, DataU[0]) - FMath::Min(FrameConfig.U, DataU[0]);
const int VDelta = FMath::Max(FrameConfig.V, DataV[0]) - FMath::Min(FrameConfig.V, DataV[0]);
const int Tolerance = 10;
// Match pixel values within a tolerance as compression can result in color variations, but not much as this is a solid color.
if (YDelta > Tolerance || UDelta > Tolerance || VDelta > Tolerance)
{
UE_LOG(LogPixelStreaming2RTC, Error, TEXT("%s"), *PixelTestString);
}
else
{
UE_LOG(LogPixelStreaming2RTC, Log, TEXT("%s"), *PixelTestString);
}
// So we can use this sink for this test again if we want.
VideoSink->ResetReceivedFrame();
return true;
}
double DeltaTime = FPlatformTime::Seconds() - StartTime;
if (DeltaTime > TimeoutSeconds)
{
UE_LOG(LogPixelStreaming2RTC, Error, TEXT("Timed out waiting to receive a frame of video through the video sink."));
return true;
}
return false;
}
bool FWaitForDataChannelOrTimeout::Update()
{
if (OutPlayer->DataChannelAvailable())
{
return true;
}
double DeltaTime = FPlatformTime::Seconds() - StartTime;
if (DeltaTime > TimeoutSeconds)
{
UE_LOG(LogPixelStreaming2RTC, Error, TEXT("Timed out waiting to data channel to be available."));
return true;
}
return false; // Not connected or timed out so run this latent test again next frame
}
bool FWaitForDataChannelMessageOrTimeout::Update()
{
double DeltaTime = FPlatformTime::Seconds() - StartTime;
if (DeltaTime > TimeoutSeconds)
{
UE_LOG(LogPixelStreaming2RTC, Error, TEXT("Player timed out waiting for a datachannel message."));
return true;
}
return *bComplete.Get(); // Not connected or timed out so run this latent test again next frame
}
bool FWaitForPlayerTrackOrTimeout::Update()
{
double DeltaTime = FPlatformTime::Seconds() - StartTime;
if (DeltaTime > TimeoutSeconds)
{
UE_LOG(LogPixelStreaming2RTC, Error, TEXT("Player timed out waiting for a track."));
return true;
}
return *bComplete.Get();
}
bool FWaitAndCheckBool::Update()
{
double DeltaTime = FPlatformTime::Seconds() - StartTime;
if (DeltaTime > WaitSeconds)
{
if ((*bCheck.Get()) != bExpectedValue)
{
UE_LOGFMT(LogPixelStreaming2RTC, Error, "FWaitAndCheckBool failed. Expected [{0}] but got [{1}]", bExpectedValue, *bCheck.Get());
}
return true;
}
return false; // We keep updating this until the timeout is complete and then we check that there's no tracks added
}
bool FWAitForBoolOrTimeout::Update()
{
double DeltaTime = FPlatformTime::Seconds() - StartTime;
if (DeltaTime > WaitSeconds)
{
if ((*bCheck.Get()) != bExpectedValue)
{
UE_LOGFMT(LogPixelStreaming2RTC, Error, "{0} failed. Expected [{1}] but got [{2}]", CheckName, bExpectedValue, *bCheck.Get());
}
return true;
}
if ((*bCheck.Get()) == bExpectedValue)
{
return true;
}
return false; // We keep updating this until the timeout is complete and then we check that there's no tracks added
}
bool FWaitAndCheckStreamerBool::Update()
{
double DeltaTime = FPlatformTime::Seconds() - StartTime;
if (DeltaTime > WaitSeconds)
{
if ((*bCheck.Get()) != bExpectedValue)
{
UE_LOGFMT(LogPixelStreaming2RTC, Error, "{0} failed. Expected [{1}] but got [{2}]", CheckName, bExpectedValue, *bCheck.Get());
}
return true;
}
return false; // We keep updating this until the timeout is complete and then we check that there's no tracks added
}
bool FWaitForStreamerDataChannelMessageOrTimeout::Update()
{
double DeltaTime = FPlatformTime::Seconds() - StartTime;
if (DeltaTime > TimeoutSeconds)
{
UE_LOG(LogPixelStreaming2RTC, Error, TEXT("Streamer timed out waiting for a datachannel message."));
return true;
}
return *bComplete.Get(); // Not connected or timed out so run this latent test again next frame
}
bool FSubscribePlayerAfterStreamerConnectedOrTimeout::Update()
{
if (OutPlayer->Subscribe(StreamerName))
{
return true;
}
double DeltaTime = FPlatformTime::Seconds() - StartTime;
if (DeltaTime > TimeoutSeconds)
{
UE_LOG(LogPixelStreaming2RTC, Error, TEXT("Timed out waiting to subscribe player."));
return true;
}
return false; // Not connected or timed out so run this latent test again next frame
}
bool FCleanupAll::Update()
{
if (OutPlayer)
{
OutPlayer.Reset();
}
if (OutStreamer)
{
OutStreamer->StopStreaming();
OutStreamer.Reset();
}
if (OutSignallingServer)
{
OutSignallingServer->Stop();
OutSignallingServer.Reset();
}
// Restore media directions back to default
SetMediaDirection(EMediaType::Audio, EMediaDirection::Bidirectional);
SetMediaDirection(EMediaType::Video, EMediaDirection::Bidirectional);
return true;
}
bool FCleanupAllPlayers::Update()
{
for(TSharedPtr<FMockPlayer>& Player : OutPlayers)
{
if (Player)
{
Player.Reset();
}
}
if (OutStreamer)
{
OutStreamer->StopStreaming();
OutStreamer.Reset();
}
if (OutSignallingServer)
{
OutSignallingServer->Stop();
OutSignallingServer.Reset();
}
// Restore media directions back to default
SetMediaDirection(EMediaType::Audio, EMediaDirection::Bidirectional);
SetMediaDirection(EMediaType::Video, EMediaDirection::Bidirectional);
return true;
}
bool FExecuteLambda::Update()
{
Func();
return true;
}
/* ---------- Utility functions ----------- */
void SetCodec(EVideoCodec Codec)
{
// Set codec
UE::PixelStreaming2::DoOnGameThreadAndWait(MAX_uint32, [Codec]() {
UPixelStreaming2PluginSettings::CVarEncoderCodec.AsVariable()->Set(*UE::PixelStreaming2::GetCVarStringFromEnum(Codec));
});
}
void SetMediaDirection(EMediaType MediaType, EMediaDirection Direction)
{
bool bTransmit = Direction == EMediaDirection::SendOnly || Direction == EMediaDirection::Bidirectional;
bool bReceive = Direction == EMediaDirection::RecvOnly || Direction == EMediaDirection::Bidirectional;
DoOnGameThreadAndWait(MAX_uint32, [MediaType, bTransmit, bReceive]() {
if (MediaType == EMediaType::Audio)
{
UPixelStreaming2PluginSettings::CVarWebRTCDisableTransmitAudio->SetWithCurrentPriority(!bTransmit);
UPixelStreaming2PluginSettings::CVarWebRTCDisableReceiveAudio->SetWithCurrentPriority(!bReceive);
}
else if (MediaType == EMediaType::Video)
{
UPixelStreaming2PluginSettings::CVarWebRTCDisableTransmitVideo->SetWithCurrentPriority(!bTransmit);
UPixelStreaming2PluginSettings::CVarWebRTCDisableReceiveVideo->SetWithCurrentPriority(!bReceive);
}
});
}
TSharedPtr<IPixelStreaming2Streamer> CreateStreamer(const FString& StreamerName, int StreamerPort)
{
TSharedPtr<IPixelStreaming2Streamer> OutStreamer = IPixelStreaming2Module::Get().CreateStreamer(StreamerName);
OutStreamer->SetVideoProducer(FVideoProducer::Create());
OutStreamer->SetConnectionURL(FString::Printf(TEXT("ws://127.0.0.1:%d"), StreamerPort));
OutStreamer->OnStreamingStarted().AddLambda([](IPixelStreaming2Streamer*) {
UE_LOG(LogPixelStreaming2RTC, Verbose, TEXT("CreateStreamer: Streamer Connected"));
});
return OutStreamer;
}
TSharedPtr<FMockPlayer> CreatePlayer(FMockPlayerConfig Config)
{
TSharedPtr<FMockPlayer> OutPlayer = FMockPlayer::Create(Config);
return OutPlayer;
}
TSharedPtr<UE::PixelStreaming2Servers::IServer> CreateSignallingServer(int StreamerPort, int PlayerPort)
{
// Make signalling server
TSharedPtr<UE::PixelStreaming2Servers::IServer> OutSignallingServer = UE::PixelStreaming2Servers::MakeSignallingServer();
UE::PixelStreaming2Servers::FLaunchArgs LaunchArgs;
LaunchArgs.ProcessArgs = FString::Printf(TEXT("--StreamerPort=%d --HttpPort=%d"), StreamerPort, PlayerPort);
bool bLaunchedSignallingServer = OutSignallingServer->Launch(LaunchArgs);
if (!bLaunchedSignallingServer)
{
UE_LOG(LogPixelStreaming2RTC, Error, TEXT("Failed to launch signalling server."));
}
UE_LOG(LogPixelStreaming2RTC, Log, TEXT("Signalling server launched=%s"), bLaunchedSignallingServer ? TEXT("true") : TEXT("false"));
return OutSignallingServer;
}
} // namespace UE::PixelStreaming2
#endif // WITH_DEV_AUTOMATION_TESTS