找回密码
 立即注册
查看: 449|回复: 0

Unreal+Agora pixel streaming

[复制链接]
发表于 2020-12-5 18:53 | 显示全部楼层 |阅读模式
前言

unreal官方推出了Pixel Streaming Demo(unreal 像素流),利用像素流送可以在用户不可见的电脑上远程运行虚幻引擎应用程序。举例而言,这台电脑可以是机构中的一台实体电脑,也可以是云端服务提供的虚拟机。虚幻引擎将使用该电脑可用的资源(CPU、GPU、内存等)来运行游戏逻辑并渲染每一帧。它会不断将此渲染输出编码到一个媒体流送中,再通过一个轻量级的网页服务堆栈进行传递。用户即可在其他电脑和移动设备上运行的标准网页浏览器中查看直播流送。

已经有其他文章详细介绍了像素流送系统的原理和相关实践,简单列一下:
    对UE4 Pixel Streaming功能的一点研究.UE4 Pixel Streaming 详细解读.
背景

如果想要将一个非常消耗CPU和GPU资源的project,推送到远端的手机或者网页上,我们需要一种流式服务,编码每一帧视频流以及音频,通过网络传输到手机上,并能将手机端的交互操作传输回本地,然后应用到项目中。在实际测试中,利用unreal官方提供的pixelstreaming插件,可以非常轻松的实现上述目标,并能在内网以及借助内网穿透在外网访问。
然而在实际测试中,我们发现利用unreal的方案在某些型号的手机,以及某些网络下,访问server失败(可能是单纯因为我们的服务器知识缺乏,配置错误导致的),不得已下,我们希望省去自己配置server的方案,而是本地编码每一帧+成熟的推流服务来实现pixel streaming,最后选用了 Agora。
本文主要介绍两点:
    unreal项目中集成Agora 视频传输SDK如何高效的编码每一帧
主要内容

Agora SDK
Agora 实时音视频(Agora Video Call)基于 UDP 协议以及声网自研的音视频编解码技术,提供可靠的实时音视频服务。Agora网上能找到的sample和官网sample都是捕捉camera的帧,而我们是将unreal中的backbuffer push给agora,两者原理应该是类似的,但是相关API完全不同,故先简单介绍下如何集成。
    首先到Agora官网下载视频通话/视频互动直播 SDK windows x64版本在项目中添加一个空白插件AgoraStreaming,并将agora sdk的dll和lib、include文件添加到build.cs中,详细步骤可参考 UE4利用插件(Plugins)导入Dll和.so我们通过 UAgoraSceneComponent来管理相关的api,这样只要在任意的actor上挂载该component即可实现pixel streaming,不需要改变原项目的结构Init阶段
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中。
VideoCapture

跟pixel streaming插件一致,我们读取back buffer的rawdata做为视频源,然后将rawdata组装成Agora的数据结构 agora::media::ExternalVideoFrame,并通过m_mediaEngine->pushVideoFrame(frame),发送出去。
    首先在UAgoraComponentBeginPlay中绑定回调,这个函数是当slate render finished的时候一个回调,方便我们拿到Back buffer
// subscribe to engine delegates here for init / framebuffer creation / whatever
        if (FSlateApplication::IsInitialized())
        {
                FSlateApplication::Get().GetRenderer()->OnBackBufferReadyToPresent().AddUObject(this, &UAgoraComponent::OnBackBufferReady_RenderThread);
        }
2. 在OnBackBufferReady_RenderThread中,我们利用RHICmdList.ReadSurfaceData拿到Backbuffer的rawdata并发送给Agora,大概代码如下所示:
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;
3. 上述代码有两个问题:(a) ReadSurfaceData非常耗时;(b)组装并发送ExternalVideoFrame没必要在renderthread;针对这两个问题,下篇文章将会进行优化,本文主要是提供全流程的基础操作。
AudioCapture

音频的捕获,我们依旧参考pixelstreaming插件,通过添加一个监听器,将捕获的音频数据实时的push到Agora中。
    首先添加一个继承自 ***ISubmixBufferListener***的监听类FSTAudioCapture实现一个Init函数,注册监听函数到audio device
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;
}
    实现 ISubmixBufferListener interface,并将音频push到Agora
void FSTAudioCapture::OnNewSubmixBuffer(const USoundSubmix* OwningSubmix, float* AudioData, int32 NumSamples, int32 InNumChannels, const int32 InSampleRate, double AudioClock)
{
        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;
        }
        UE_LOG(LogTemp, VeryVerbose, TEXT("captured %d samples, %dc, %dHz"), NumSamples, NumChannels, SampleRate);
        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++;
        }
        /*FString fileStr = "testccj"+ FString::FromInt(idx++)+".pcm";
        FFileHelper::SaveArrayToFile(PCM16, *fileStr);*/
        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
        //UE_LOG(LogTemp, Log, TEXT("RecordingBuffer.SIZE():%d"), RecordingBuffer.Num());
        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接收的音频数据格式必须是PCM16,因此我们通过一个for循环进行了音频的转换;发送音频时,是将接收到的整个音频buffer切分成10ms的chunks进行发送;另外音频的主要参数samplerate和channels可以任意设定,只要跟Agora Init中设置的保持一致即可。OnNewSubmixBuffer,要起作用,需要进行一点设置:在Level Editor > Play中,找到 Additional Launch Parameters 添加一个参数-AudioMixer即可
前端联调

在完成本地音频和视频的捕捉后,怎么验证我们成功捕捉到了呢?这里我们通过简单的前端网页,然后加入到我们项目中一个agora channels即可看到效果。
    首先到Agora官网下载视频通话/视频互动直播 SDK的web 版sdk在web 版sdk的目录下,利用python简单的开个网页服务器
python -m http.server 88883. 然后在chrome浏览器中输入“127.0.0.1”,如果正常的话应该看到如下界面:
4. 在APPID和channel中对应输入项目工程中的appid和channel
5. 然后运行游戏,就可以看到效果啦
总结

我们利用Agora的视频互动sdk实现了unreal应用的像素流推送,能够向任何地方的任何设备提供高质量UE4内容,可以在功能强大的远程计算机(在云端或者本地服务器)上运行你的虚幻引擎应用程序,利用它的所有资源——CPU、GPU、内存,等等——实时执行游戏逻辑并渲染每一帧画面。然后最终用户可以在他们自己的计算机、平板电脑或智能手机上使用标准的Web浏览器。向云游戏,云渲染迈出了一小步,哈哈哈
谢谢!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-20 06:29 , Processed in 0.064797 second(s), 23 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表