也可以叫做unreal应用的屏幕录制0.0
unreal官方推出了Pixel Streaming Demo (unreal 像素流),利用像素流送可以在用户不可见的电脑上远程运行虚幻引擎应用程序。举例而言,这台电脑可以是机构中的一台实体电脑,也可以是云端服务提供的虚拟机。虚幻引擎将使用该电脑可用的资源(CPU、GPU、内存等)来运行游戏逻辑并渲染每一帧。它会不断将此渲染输出编码到一个媒体流送中,再通过一个轻量级的网页服务堆栈进行传递。用户即可在其他电脑和移动设备上运行的标准网页浏览器中查看直播流送。
已经有其他文章详细介绍了像素流送系统的原理和相关实践,简单列一下:
如果想要将一个非常消耗CPU和GPU资源的project,推送到远端的手机或者网页上,我们需要一种流式服务,编码每一帧视频流以及音频,通过网络传输到手机上,并能将手机端的交互操作传输回本地,然后应用到项目中。在实际测试中,利用unreal官方提供的pixelstreaming插件,可以非常轻松的实现上述目标,并能在内网以及借助内网穿透在外网访问。
然而在实际测试中,我们发现利用unreal的方案在某些型号的手机,以及某些网络下,访问server失败(可能是单纯因为我们的服务器知识缺乏,配置错误导致的),不得已下,我们希望省去自己配置server的方案,而是本地编码每一帧+成熟的推流服务来实现pixel streaming,最后选用了 Agora。
本文主要介绍两点:
Agora 实时音视频(Agora Video Call)基于 UDP 协议以及声网自研的音视频编解码技术,提供可靠的实时音视频服务。Agora网上能找到的sample和官网sample都是捕捉camera的帧,而我们是将unreal中的backbuffer push给agora,两者原理应该是类似的,但是相关API完全不同,故先简单介绍下如何集成。
void UAgoraComponent::InitAgora(const FString& Agora_rtc_appid)
{
RtcEnginePtr = TSharedPtr<FAgoraRtcEngine>(FAgoraRtcEngine::createAgoraRtcEngine());
static agora::rtc::RtcEngineContext ctx;
ctx.appId = TCHAR_TO_ANSI(*Agora_rtc_appid);
ctx.eventHandler = new RtcEngineEventHandler();
int ret = RtcEnginePtr->initialize(ctx);
int nRet = RtcEnginePtr->enableVideo();
nRet = RtcEnginePtr->enableAudio();
nRet = RtcEnginePtr->setChannelProfile(agora::rtc::CHANNEL_PROFILE_LIVE_BROADCASTING);
nRet = RtcEnginePtr->setClientRole(agora::rtc::CLIENT_ROLE_BROADCASTER);
nRet = RtcEnginePtr->setAudioProfile(agora::rtc::AUDIO_PROFILE_MUSIC_HIGH_QUALITY, agora::rtc::AUDIO_SCENARIO_SHOWROOM);
RtcEnginePtr->adjustRecordingSignalVolume(200);
RtcEnginePtr->enableWebSdkInteroperability(true);
RtcEnginePtr->setExternalVideoSource(true, false);
RtcEnginePtr->setExternalAudioSource(true, 48000, 2);
agora::rtc::VideoEncoderConfiguration conf;
conf.dimensions = agora::rtc::VideoDimensions(Width, Height);
conf.frameRate = FrameRate;
conf.degradationPreference = agora::rtc::MAINTAIN_FRAMERATE;
conf.orientationMode = agora::rtc::ORIENTATION_MODE_FIXED_PORTRAIT;
}
-在初始化结束后,即可调用 joinChannel,到这里跟Agora已经连上了,后续我们需要将每帧的内容和音频实时的push到Agora中。
跟pixel streaming插件一致,我们读取back buffer的rawdata做为视频源,然后将rawdata组装成Agora的数据结构 agora::media::ExternalVideoFrame,并通过m_mediaEngine->pushVideoFrame(frame),发送出去。
// subscribe to engine delegates here for init / framebuffer creation / whatever
if (FSlateApplication::IsInitialized())
{
FSlateApplication::Get().GetRenderer()->OnBackBufferReadyToPresent().AddUObject(this, &UAgoraComponent::OnBackBufferReady_RenderThread);
}
check(IsInRenderingThread());
FRHICommandListImmediate& RHICmdList = FRHICommandListExecutor::GetImmediateCommandList();
auto width = BackBuffer->GetSizeX();
auto height = BackBuffer->GetSizeY();
FIntRect Rect(0, 0, BackBuffer->GetSizeX(), BackBuffer->GetSizeY());
TArray<FColor> Data;
RHICmdList.ReadSurfaceData(BackBuffer, Rect, Data, FReadSurfaceDataFlags());
SetResolution(width, height, agora::rtc::FRAME_RATE_FPS_30);
agora::media::ExternalVideoFrame* frame = new agora::media::ExternalVideoFrame();
frame->type = agora::media::ExternalVideoFrame::VIDEO_BUFFER_RAW_DATA;
frame->format = agora::media::ExternalVideoFrame::VIDEO_PIXEL_BGRA;
frame->buffer = Data.GetData();
frame->stride = BackBuffer->GetSizeX();
frame->height = BackBuffer->GetSizeY();
frame->rotation = 0;
frame->timestamp = timestamp++;
RtcEnginePtr->pushVideoFrame1(frame);
delete frame;
音频的捕获,我们依旧参考pixelstreaming插件,通过添加一个监听器,将捕获的音频数据实时的push到Agora中。
int32 FSTAudioCapture::Init()
{
if (bInitialized)
return 0;
// subscribe to audio data
if (!GEngine)
{
return -1;
}
FAudioDevice* AudioDevice = GEngine->GetMainAudioDevice();
if (!AudioDevice)
{
return -1;
}
bInitialized = true;
AudioDevice->RegisterSubmixBufferListener(this);
bRecordingInitialized = true;
UE_LOG(LogTemp, Log, TEXT("[audio capture] Init"));
return 0;
}
if (!(bInitialized && bRecordingInitialized))
{
return;
}
// Only 48000hz supported for now
if (InSampleRate != SampleRate)
{
// Only report the problem once
if (!bFormatChecked)
{
bFormatChecked = true;
UE_LOG(LogTemp, Error, TEXT("Audio samplerate needs to be 48000hz"));
}
return;
}
Audio::TSampleBuffer<float> Buffer(AudioData, NumSamples, InNumChannels, SampleRate);
// Mix to stereo if required, since PixelStreaming only accepts stereo at the moment
if (Buffer.GetNumChannels() != NumChannels)
{
Buffer.MixBufferToChannels(NumChannels);
}
// Convert to signed PCM 16-bits
PCM16.Reset(Buffer.GetNumSamples());
PCM16.AddZeroed(Buffer.GetNumSamples());
const float* Ptr = reinterpret_cast<const float*>(Buffer.GetData());
for (int16& S : PCM16)
{
int32 N = *Ptr >= 0 ? *Ptr * int32(MAX_int16) : *Ptr * (int32(MAX_int16) + 1);
S = static_cast<int16>(FMath::Clamp(N, int32(MIN_int16), int32(MAX_int16)));
Ptr++;
}
RecordingBuffer.Append(reinterpret_cast<const uint8*>(PCM16.GetData()), PCM16.Num() * sizeof(PCM16[0]));
int BytesPer10Ms = (SampleRate * NumChannels * static_cast<int>(sizeof(uint16))) / 100;
// Feed in 10ms chunks
while (RecordingBuffer.Num() >= BytesPer10Ms)
{
{
FScopeLock Lock(&DeviceBufferCS);
auto RTCEngine = AgoraComp->RtcEnginePtr;
if (RTCEngine.Get())
{
agora::media::IAudioFrameObserver::AudioFrame externalAudioFrame;
externalAudioFrame.type = agora::media::IAudioFrameObserver::FRAME_TYPE_PCM16;
externalAudioFrame.samples = BytesPer10Ms/ (sizeof(uint16) * NumChannels);
externalAudioFrame.bytesPerSample = 2;
externalAudioFrame.channels = NumChannels;
externalAudioFrame.samplesPerSec = SampleRate;
externalAudioFrame.buffer = RecordingBuffer.GetData();
externalAudioFrame.renderTimeMs = 10;
RTCEngine->pushAudioFrame1(&externalAudioFrame);
}
}
RecordingBuffer.RemoveAt(0, BytesPer10Ms, false);
}
注意两点:
在完成本地音频和视频的捕捉后,怎么验证我们成功捕捉到了呢?这里我们通过简单的前端网页,然后加入到我们项目中一个agora channels即可看到效果。
python -m http.server 8888
我们利用Agora的视频互动sdk实现了unreal应用的像素流推送,能够向任何地方的任何设备提供高质量UE4内容,可以在功能强大的远程计算机(在云端或者本地服务器)上运行你的虚幻引擎应用程序,利用它的所有资源——CPU、GPU、内存,等等——实时执行游戏逻辑并渲染每一帧画面。然后最终用户可以在他们自己的计算机、平板电脑或智能手机上使用标准的Web浏览器。向云游戏,云渲染迈出了一小步,哈哈哈。
谢谢