本文内容

在本文中我将介绍如何将Vuforia的视频共享给Agora系统,利用声网提供的免费平台将实时视频内容共享出去。这次的案例中涉及两个技术点,一个是Vuforia的视频接口获取,一个是Agora系统以外部视频源的方式进行实时视频的传输。同时我们也通过这个小案例来熟悉两款SDK的使用。

SDK简介

本篇文章我们将介绍两款SDK:

第一款是声网的SDK,主要功能是进行实时视音频的传输、消息传输,兼容 iOS、Android、Windows、macOS、Web、小程序、RTOS、Flutter、Electron、React Native、Unity、Unreal 等 20 大开发平台。对于注册用户,每个月均有10000分钟的免费时长,这对于普通开发者进行如那件的开发和测试已经是足够的了,并且实测在4G网络的情况下端到端延迟<400ms,开发测试也是十分良好的体验,同时还有免费的后台管理系统对应用进行监控,这个后面用到时候再细说。

第二款是Vuforia的SDK,这是一款增强现实的底层SDK,也是可以免费使用的,是以Licence授权形式授权用户使用,对于没有购买Licence的开发者在视频画面的左下角会有“Vufiria”标志,当让对于开发者来说问题不大。

开发环境

主机:windows 10

引擎:Unity 3D 2019.3.1、Visual Studio 2015

实现步骤

我们将通过如下方式进行本次项目的实现。首先Vuforia支持以注册事件的方式回调自身因为AR所独占的视频数据,而Agora本身支持独占相机资源也支持以外部视频源的方式进行直播,我们需要将这两部分结合起来。同时,做这个视频共享的目的还有一个,就是将Unity 3D渲染的3D效果也同步直播出去,这才是最根本的目的。

第一步: 导入SDK

(1)打开Unity 3D,创建工程。进入Unity的官方商城,分别获取到Vuforia的开发SDK和Agora的开发SDK

顺利导入之后,简单看一下结构:将所有相关内容作为一个Package放到一个文件夹是很好的习惯,不用担心里面的Plugins,系统会自动寻找其中的插件。基于Unity平台,目前Agora支持安卓、Iphone、Mac、Win32/64,不过还没有基于Unity3D平台的Html5的插件,期待未来能有实现(毕竟比较擅长Unity,其他不太熟悉)

使用Agora要使用licence,下面看一下注册和licence申请:

网址:https://www.agora.io/cn/

注册:输入相关信息

申请licence:成功登录尽可进入项目页面,在项目页面中即可创建项目(记得实名制哦)。

水晶球:项目的详细数据分析,这个功能目前我们还用不到,后面再介绍

(2)再来简单看一下需要配合来做的Vuforia。前面介绍过了,这是一款可以给开发者免费试用(加水印)的增强现实SDK,同样支持多平台的使用。

需要注意的是,我们同样需要申请licence,地址:https://developer.vuforia.com/。

打开“0-Main”场景,在下图的左下角点击“Open Vuforia…”即可进入到相关配置页面,即输入licence的界面。

(3)小结:熟悉了本教程所用的两种SDK及其结构、功能、注册和Licence申请,对于这两种SDK有各种技术能力,感兴趣的同学可以在官方网站多停留一会。

第二步: Vuforia的准备工作

这一步中我们将完成一个AR场景的搭建,完成Vuforia视频的获取、屏幕截屏作为后期Agora视频源的输入。

(1)搭建AR场景:打开上图中“0-Main”场景,完成licence输入,选中场景中“ImageTarget”,在右侧“Image Target Behaviour”中选中如下内置识别图

(注意:首次选择From Database 会提示导入数据,请不要拒绝^_^,其次记得电脑上查摄像头)。点击运行或者CTRL+P运行,将摄像头对准识别图(识别图在Assets/Editor/Vuforia/ImageTargetTextures/VuforiaMars_Images/)即可看到模型在图片上。

(2)编写代码:简单介绍一下,这个脚本中我们使用了Vuforia提供的注册回调机制,将自身从相机获取的视频共享出来。要注意一下这其中比较重要的像素格式,同样在Agora系统中也会有像素格式的问题。

A:透明度 R:红色 G:绿 B:蓝

ARGB_4444:每个像素占四位,即A=4,R=4,G=4,B=4,那么一个像素点占4+4+4+4=16位

ARGB_8888:每个像素占四位,即A=8,R=8,G=8,B=8,那么一个像素点占8+8+8+8=32位

RGB_565:每个像素占四位,即R=5,G=6,B=5,没有透明度,那么一个像素点占5+6+5=16位

GrayScale:像素占四位,即R=G=B=gray

如下,“TakeScreen”函数是对整个Scene中一个Camera场景进行截图,目的是获取渲染后的相机效果。

using System.Collections;
using System.Collections.Generic;
using agora_gaming_rtc;
using UnityEngine;
using UnityEngine.UI;
using Vuforia;

public class VuforiaImageShare : MonoBehaviour
{
    public UnityEngine.UI.RawImage tex;
    public UnityEngine.UI.RawImage screenImage;
    public Camera renderCamera;
    #region PRIVATE_MEMBERS
    private PIXEL_FORMAT mPixelFormat = PIXEL_FORMAT.UNKNOWN_FORMAT;
    private bool mFormatRegistered = false;
    #endregion // PRIVATE_MEMBERS
    #region MONOBEHAVIOUR_METHODS
    //作为共享的图片数据
    Texture2D mTexture;
    Texture2D screenShot;
    Rect mRect;
    private RenderTexture renderTex;
    //启用相机画面或者屏幕渲染画面
    public bool useCam = true;
    void Start()
    {
        //注册的图片返回格式,有多种格式可选GRAYSCALE/RGBA8888/RGB565等常见格式
#if UNITY_EDITOR
        mPixelFormat = PIXEL_FORMAT.RGBA8888; // Need Grayscale for Editor 经测试,在Editor模式同样可以RGBA8888
#else
           mPixelFormat = PIXEL_FORMAT.RGB888; // Use RGB888 for mobile
#endif
        //注册回调
        // Register Vuforia life-cycle callbacks:
        VuforiaARController.Instance.RegisterVuforiaStartedCallback(OnVuforiaStarted);
        VuforiaARController.Instance.RegisterTrackablesUpdatedCallback(OnTrackablesUpdated);
        VuforiaARController.Instance.RegisterOnPauseCallback(OnPause);
        #endregion // MONOBEHAVIOUR_METHODS
        // 创建一个RenderTexture对象  
        renderTex = new RenderTexture((int)Screen.width, (int)Screen.height, 0);
    }


    void Update()
    {
        StartCoroutine(TakeScreen());
    }
    public Texture2D GetSharedImage()
    {
        if (useCam)
            return mTexture;
        return null;
    }
  
    private void OnVuforiaStarted()
    {
        // Vuforia has started, now register camera image format  
        if (CameraDevice.Instance.SetFrameFormat(mPixelFormat, true))
        {
            Debug.Log("Successfully registered pixel format " + mPixelFormat.ToString());
            mFormatRegistered = true;
        }
        else
        {
            Debug.LogError(
              "Failed to register pixel format " + mPixelFormat.ToString() +
              "\n the format may be unsupported by your device;" +
              "\n consider using a different pixel format.");

            mFormatRegistered = false;
        }
    }


    /// Called each time the Vuforia state is updated
    /// unitunity
    void OnTrackablesUpdated()
    {
        if (mFormatRegistered)
        {
            if (useCam)
            {
                Vuforia.Image image = CameraDevice.Instance.GetCameraImage(mPixelFormat);
                if (image != null)
                {
                    /*                      Debug.Log(
                                              "\nImage Format: " + image.PixelFormat +
                                              "\nImage Size:   " + image.Width + "x" + image.Height +
                                              "\nBuffer Size:  " + image.BufferWidth + "x" + image.BufferHeight +
                                              "\nImage Stride: " + image.Stride + "\n"
                                          );*/
                    //当分辨率变化时候,进行调整
                    if (mTexture == null || mTexture.width != image.Width)
                    {
                        mTexture = new Texture2D((int)image.Width, (int)image.Height, TextureFormat.RGBA32, false);
                    }
                    byte[] pixels = image.Pixels;
                    if (pixels != null && pixels.Length > 0)
                    {
                        // Debug.Log("\nImage pixels: " + pixels.Length);
                        /*   Debug.Log(
                               "\nImage pixels: " +
                               pixels[0] + ", " +
                               pixels[1] + ", " +
                               pixels[2] + ", ...\n"
                           );*/
                        //    toast.text = "x: " + x + "y:" + y + "b:" + bytes.Length;

                        // 转化为Texture2D
                          image.CopyToTexture(mTexture);
                        // mTexture.SetPixels32(ImageToColor32(image));
                      //   mTexture.SetPixels32(image);
                        //   mTexture.Apply();
                        tex.texture = mTexture;
                        //   byte[] bytes = mTexture.GetRawTextureData();
                        //    ShareCam(image.Width, image.Height, bytes);

                    }
                }
            }
        }
    }
    IEnumerator TakeScreen()
    {
        yield return new WaitForEndOfFrame();

        if (renderCamera == null)
            yield return 0;
        //        Debug.Log(_rt.width+"||"+_rt.height);
        if (screenShot == null || screenShot.width != renderTex.width)
            screenShot = new Texture2D(renderTex.width, renderTex.height, TextureFormat.RGBA32, false);
        renderCamera.targetTexture = renderTex;
        renderCamera.Render();
        RenderTexture.active = renderTex;
        screenShot.ReadPixels(new Rect(0, 0, renderTex.width, renderTex.height), 0, 0);
        // 重置相关参数,以使用camera继续在屏幕上显示  
        renderCamera.targetTexture = null;
        //ps: camera2.targetTexture = null;  
        RenderTexture.active = null; // JC: added to avoid errors  
        screenShot.Apply();
        screenImage.texture = screenShot;
   
    }
    //RGBA8888 to RGBA32
    Color32[] ImageToColor32(Vuforia.Image a)
    {
        Color32[] r = new Color32[a.BufferWidth * a.BufferHeight];
        for (int i = 0; i < r.Length; i++)
        {
            r[i].b = a.Pixels[i * 3];
            r[i].g = a.Pixels[i * 3 + 1];
            r[i].r = a.Pixels[i * 3 + 2];
            r[i].a = 1;
        }
        return r;
    }
    /// 
    /// Called when app is paused / resumed
    /// 
    void OnPause(bool paused)
    {
        if (paused)
        {
            Debug.Log("App was paused");
            UnregisterFormat();
        }
        else
        {
            Debug.Log("App was resumed");
            RegisterFormat();
        }
    }
    /// 
    /// Register the camera pixel format
    /// 
    void RegisterFormat()
    {
        if (CameraDevice.Instance.SetFrameFormat(mPixelFormat, true))
        {
            Debug.Log("Successfully registered camera pixel format " + mPixelFormat.ToString());
            mFormatRegistered = true;
        }
        else
        {
            Debug.LogError("Failed to register camera pixel format " + mPixelFormat.ToString());
            mFormatRegistered = false;
        }
    }
    /// 
    /// Unregister the camera pixel format (e.g. call this when app is paused)
    /// 
    void UnregisterFormat()
    {
        Debug.Log("Unregistering camera pixel format " + mPixelFormat.ToString());
        CameraDevice.Instance.SetFrameFormat(mPixelFormat, false);
        mFormatRegistered = false;
    }
}

将以上脚本挂在任一场景中GameObject上,拖入对应的参数脚本运行程序,可以看到左下角为屏幕实时渲染的截图(包含渲染过的3D模型),右下角为实时获取的Vuforia实时回调图片。

(3)小结:至此我们完成了对于Agora数据的录入准备工作,包括视频原始数据和经过渲染的屏幕数据(较重要),后期在视频会议、远程协助等方面可以有比较多的应用。

第三步: Agora应用

我们在这一步将熟悉Agora的流程,跑通demo,学习外部视频源输入的代码。

(1)学习Demo场景。在如下场景中,输入按照之前步骤在agora官网创建项目获得的AppID,点击运行即可进入视频房间(是不是很简单(^▽^))。

下面简单介绍几个重要函数:打开脚本“TestHelloUnityVideo”这里面的“loadEngine”即为Agora引擎重要的初始化步骤。

 public void loadEngine(string appId)
    {
        // start sdk
        Debug.Log("initializeEngine");

        if (mRtcEngine != null)
        {
            Debug.Log("Engine exists. Please unload it first!");
            return;
        }

        // init engine
        mRtcEngine = IRtcEngine.GetEngine(appId);

        // enable log
        mRtcEngine.SetLogFilter(LOG_FILTER.DEBUG | LOG_FILTER.INFO | LOG_FILTER.WARNING | LOG_FILTER.ERROR | LOG_FILTER.CRITICAL);
    }

下面的“join”函数,为用户加入房间以及各种回调函数的设计:

 public void join(string channel)
    {
        Debug.Log("calling join (channel = " + channel + ")");

        if (mRtcEngine == null)
            return;

        // set callbacks (optional)
        mRtcEngine.OnJoinChannelSuccess = onJoinChannelSuccess;
        mRtcEngine.OnUserJoined = onUserJoined;
        mRtcEngine.OnUserOffline = onUserOffline;

        // enable video
        mRtcEngine.EnableVideo();
        // allow camera output callback
        mRtcEngine.EnableVideoObserver();

        // join channel
        mRtcEngine.JoinChannel(channel, null, 0);
    }

接下来的“leave”函数,切记在程序退出时记得关闭,不然可能会导致一些bug。

(2)接下来,我们要完成Agora系统的外置视频源输入,有以下内容:

(a)设置Agora的SDK为外置视频源模式,在“join”函数中, mRtcEngine.SetExternalVideoSource(true, false);

请确保你是在调用pushExternalVideoFrame前已调用 setExternalVideoSource, 并将参数 pushMode 设为 true ,不然调用本方法后会一直报错。

    public void join(string channel)
    {
        Debug.Log("calling join (channel = " + channel + ")");

        if (mRtcEngine == null)
            return;
        // set callbacks (optional)
        mRtcEngine.OnJoinChannelSuccess = onJoinChannelSuccess;
        mRtcEngine.OnUserJoined = onUserJoined;
        mRtcEngine.OnUserOffline = onUserOffline;
        // enable video
        mRtcEngine.EnableVideo();
        // allow camera output callback
        mRtcEngine.EnableVideoObserver();

        // 配置外部视频源。
        mRtcEngine.SetExternalVideoSource(true, false);
        // join channel
        mRtcEngine.JoinChannel(channel, null, 0);
        // Optional: if a data stream is required, here is a good place to create it
        int streamID = mRtcEngine.CreateDataStream(true, true);
    }

(b)添加外部视频源推送函数,需要注意的是:

  • VIDEO_PIXEL_FORMAT.VIDEO_PIXEL_RGBA;目前已经支持多种视频格式,在unity中不必讲RGBA再转换成BGRA(赞~)
  • externalVideoFrame.cropLeft;等四个位置,这里是做视频的裁剪
  • externalVideoFrame.rotation = 0;这里在移动端具有重要作用,作为视频的旋转设置
    void ShareCam(Texture2D tex)
    {
        if (tex == null)
            return;
        byte[] bytes = tex.GetRawTextureData();
        int size = Marshal.SizeOf(bytes[0]) * bytes.Length;
        // 查询是否存在 IRtcEngine 实例。
        IRtcEngine rtc = IRtcEngine.QueryEngine();
        if (rtc != null)
        {
            // 创建外部视频帧。
            ExternalVideoFrame externalVideoFrame = new ExternalVideoFrame();
            // 设置视频帧 buffer 类型。
            externalVideoFrame.type = ExternalVideoFrame.VIDEO_BUFFER_TYPE.VIDEO_BUFFER_RAW_DATA;
            // 设置像素格式。
            externalVideoFrame.format = ExternalVideoFrame.VIDEO_PIXEL_FORMAT.VIDEO_PIXEL_RGBA;
            // 应用原始数据。
            externalVideoFrame.buffer = bytes;
            // 设置视频帧宽度(pixel)。
            externalVideoFrame.stride = tex.width;
            // 设置视频帧高度(pixel)。
            externalVideoFrame.height = tex.height;
            // 设置从哪侧移除视频帧的像素。
            externalVideoFrame.cropLeft = 0;
            externalVideoFrame.cropTop = 0;
            externalVideoFrame.cropRight = 0;
            externalVideoFrame.cropBottom = 0;
            // 设置视频帧旋转角度: 0、90、180 或 270。
            externalVideoFrame.rotation = 0;

            // 使用视频时间戳增加 i。
            externalVideoFrame.timestamp = i++;
            // 推送外部视频帧。
            int a = rtc.PushVideoFrame(externalVideoFrame);
        }
    }

(c)依据第二步我们所获得的视频图片数据源(两种哈),形成完整的Agroa外置视频源系统代码:

using System.Collections;
using System.Collections.Generic;
using agora_gaming_rtc;
using agora_utilities;
using UnityEngine;
using UnityEngine.UI;
using System.Globalization;
using System.Runtime.InteropServices;
public class AgoraVideoExtern : MonoBehaviour
{
    public VuforiaImageShare vuforiaShare;
    // instance of agora engine
    private IRtcEngine mRtcEngine;
    //输入创建项目所获得的的id
    [SerializeField]
    public string appId = "";
    //输入视频房间名称
    public string roomName="Agora";
    public InputField inputChan;
    int i = 1000;
    // Start is called before the first frame update
    void Start()
    {
        inputChan.text = roomName;
        loadEngine(appId);
    }
    //加入房间按钮
    public void JoinBtn()
    {
        join(roomName);
    }
    public void loadEngine(string appId)
    {
        // start sdk
        Debug.Log("initializeEngine");
        if (mRtcEngine != null)
        {
            Debug.Log("Engine exists. Please unload it first!");
            return;
        }
        // init engine
        mRtcEngine = IRtcEngine.GetEngine(appId);
        // enable log
        mRtcEngine.SetLogFilter(LOG_FILTER.DEBUG | LOG_FILTER.INFO | LOG_FILTER.WARNING | LOG_FILTER.ERROR | LOG_FILTER.CRITICAL);
    }
    public void join(string channel)
    {
        Debug.Log("calling join (channel = " + channel + ")");

        if (mRtcEngine == null)
            return;
        // set callbacks (optional)
        mRtcEngine.OnJoinChannelSuccess = onJoinChannelSuccess;
        mRtcEngine.OnUserJoined = onUserJoined;
        mRtcEngine.OnUserOffline = onUserOffline;
        // enable video
        mRtcEngine.EnableVideo();
        // allow camera output callback
        mRtcEngine.EnableVideoObserver();

        // 配置外部视频源。
        mRtcEngine.SetExternalVideoSource(true, false);
        // join channel
        mRtcEngine.JoinChannel(channel, null, 0);
        // Optional: if a data stream is required, here is a good place to create it
        int streamID = mRtcEngine.CreateDataStream(true, true);
    }
    void ShareCam(Texture2D tex)
    {
        if (tex == null)
            return;
        byte[] bytes = tex.GetRawTextureData();
        int size = Marshal.SizeOf(bytes[0]) * bytes.Length;
        // 查询是否存在 IRtcEngine 实例。
        IRtcEngine rtc = IRtcEngine.QueryEngine();
        if (rtc != null)
        {
            // 创建外部视频帧。
            ExternalVideoFrame externalVideoFrame = new ExternalVideoFrame();
            // 设置视频帧 buffer 类型。
            externalVideoFrame.type = ExternalVideoFrame.VIDEO_BUFFER_TYPE.VIDEO_BUFFER_RAW_DATA;
            // 设置像素格式。
            externalVideoFrame.format = ExternalVideoFrame.VIDEO_PIXEL_FORMAT.VIDEO_PIXEL_RGBA;
            // 应用原始数据。
            externalVideoFrame.buffer = bytes;
            // 设置视频帧宽度(pixel)。
            externalVideoFrame.stride = tex.width;
            // 设置视频帧高度(pixel)。
            externalVideoFrame.height = tex.height;
            // 设置从哪侧移除视频帧的像素。
            externalVideoFrame.cropLeft = 0;
            externalVideoFrame.cropTop = 0;
            externalVideoFrame.cropRight = 0;
            externalVideoFrame.cropBottom = 0;
            // 设置视频帧旋转角度: 0、90、180 或 270。
            externalVideoFrame.rotation = 0;

            // 使用视频时间戳增加 i。
            externalVideoFrame.timestamp = i++;
            // 推送外部视频帧。
            int a = rtc.PushVideoFrame(externalVideoFrame);
        }
    }
    // implement engine callbacks
    private void onJoinChannelSuccess(string channelName, uint uid, int elapsed)
    {
        Debug.Log("JoinChannelSuccessHandler: uid = " + uid);

      //  GameObject textVersionGameObject = GameObject.Find("VersionText");
      //  textVersionGameObject.GetComponent<Text>().text = "SDK Version : " + getSdkVersion();
    }

    // When a remote user joined, this delegate will be called. Typically
    // create a GameObject to render video on it
    private void onUserJoined(uint uid, int elapsed)
    {
        Debug.Log("onUserJoined: uid = " + uid + " elapsed = " + elapsed);
        // this is called in main thread
        // find a game object to render video stream from 'uid'
        GameObject go = GameObject.Find(uid.ToString());
        if (!ReferenceEquals(go, null))
        {
            return; // reuse
        }
/*
        // create a GameObject and assign to this new user
        VideoSurface videoSurface = makeImageSurface(uid.ToString());
        if (!ReferenceEquals(videoSurface, null))
        {
            // configure videoSurface
            videoSurface.SetForUser(uid);
            videoSurface.SetEnable(true);
            videoSurface.SetVideoSurfaceType(AgoraVideoSurfaceType.RawImage);
            videoSurface.SetGameFps(30);
        }
        */
    }

    public VideoSurface makePlaneSurface(string goName)
    {
        GameObject go = GameObject.CreatePrimitive(PrimitiveType.Plane);

        if (go == null)
        {
            return null;
        }
        go.name = goName;
        // set up transform
        go.transform.Rotate(-90.0f, 0.0f, 0.0f);
        float yPos = Random.Range(3.0f, 5.0f);
        float xPos = Random.Range(-2.0f, 2.0f);
        go.transform.position = new Vector3(xPos, yPos, 0f);
        go.transform.localScale = new Vector3(0.25f, 0.5f, .5f);

        // configure videoSurface
        VideoSurface videoSurface = go.AddComponent<VideoSurface>();
        return videoSurface;
    }

    private const float Offset = 100;
    public VideoSurface makeImageSurface(string goName)
    {
        GameObject go = new GameObject();

        if (go == null)
        {
            return null;
        }
        go.name = goName;

        // to be renderered onto
        go.AddComponent<RawImage>();

        // make the object draggable
        go.AddComponent<UIElementDragger>();
        GameObject canvas = GameObject.Find("Canvas");
        if (canvas != null)
        {
            go.transform.parent = canvas.transform;
        }
        // set up transform
        go.transform.Rotate(0f, 0.0f, 180.0f);
        float xPos = Random.Range(Offset - Screen.width / 2f, Screen.width / 2f - Offset);
        float yPos = Random.Range(Offset, Screen.height / 2f - Offset);
        go.transform.localPosition = new Vector3(xPos, yPos, 0f);
        go.transform.localScale = new Vector3(3f, 4f, 1f);

        // configure videoSurface
        VideoSurface videoSurface = go.AddComponent<VideoSurface>();
        return videoSurface;
    }
    private void onUserOffline(uint uid, USER_OFFLINE_REASON reason)
    {
        // remove video stream
        Debug.Log("onUserOffline: uid = " + uid + " reason = " + reason);
        // this is called in main thread
        GameObject go = GameObject.Find(uid.ToString());
        if (!ReferenceEquals(go, null))
        {
            Object.Destroy(go);
        }
    }
    public void unloadEngine()
    {
        Debug.Log("calling unloadEngine");

        // delete
        if (mRtcEngine != null)
        {
            IRtcEngine.Destroy();  // Place this call in ApplicationQuit
            mRtcEngine = null;
        }
    }
    public void leave()
    {
        Debug.Log("calling leave");

        if (mRtcEngine == null)
            return;

        // leave channel
        mRtcEngine.LeaveChannel();
        // deregister video frame observers in native-c code
        mRtcEngine.DisableVideoObserver();
    }
    void OnApplicationQuit()
    {
        leave();
        unloadEngine();

    }
    // Update is called once per frame
    void Update()
    {
        if (mRtcEngine == null)
            return;
        ShareCam(vuforiaShare.GetSharedImage());
    }
}

这一步我们将两个系统进行融合,下图是简单场景编写,详细内容会在附件连接中提供下载。下图为Agroa的AR识别场景,在开启后将可以看到AR场景并开启外部视频源的数据推送。

在此之前,我们需要将Agroa例程中的“SceneHome”输出为exe可运行程序(请记得licence和BuildSetting添加场景)。

导出后,请先运行编辑器中的AR场景,因为要抢占开发电脑的摄像头(agroa默认取第一个摄像头),创建好系统场景;

之后开启事先导出的Agroa的例程场景,输入频道号“Agora”即可,分别看一下只传输视频和传输AR视频的效果

下面是切换了AR渲染后的效果,可以明显看到在右侧客户端画面中,出现了渲染过的3D人物。

待解决的问题

(1)通过传送渲染过的相机,对计算能力的要求,目前本机I7 9700k,1050TI显卡,编辑器内可以达到60帧,是否有更高效的方法?

(2)仔细观察会发现,左侧屏幕中,Vuforia回调的视频画面是上下反转的,而传到左侧,又反转了一次;是什么问题该如何处理;

(3)仔细观察会发现,右侧原始画面在左侧展示时候,虽然压缩了但是明显的也被裁减了,这是为什么呢?

相关资源

论坛:

Agora开发者中心:https://docs.agora.io/cn

AgoraGit仓库:https://github.com/AgoraIO

本次源代码:

链接:https://pan.baidu.com/s/1DIQNFTrSUHsARhRH_KchNQ
提取码:n8ne