如何在 Android 中创建一个 AR 远程协助 APP

1_9TgWgeQ2a0NpFt8Zqx3vCg

我们经常会碰到这样的状况:当我们刚为吹风机或加热器插上电时,房间里的灯忽然全灭了,电视机也停止了工作。

这时你不得不绝望地打电话向客服求助,向他们描述刚刚发生的事情,而他们告诉你只需要打开空气开关就好了。可当你打开配电板后,却不知道哪个是空气开关。经过20分钟不断与客服确定断路器的位置后,你终于发现家中压根就没有什么所谓的空气开关。

如今大多数的远程协助都是通过语音电话或是短信进行的,这对于很多试图向客服阐述清楚问题并想弄清与其相关的新概念和专业术语的用户来说无疑是十分麻烦的。

幸运的是,现在出现了解决此类问题的视频聊天和AR技术。在本指南中,我会告诉你如何构建一个利用ARCore和视频聊天的Adroid app来创造互动体验。

要求

1.对Java和Android SDK要有基础到中级的了解。

2.能基本理解ARCore和增强现实技术的概念。

3.Agora.io开发者账号

4.硬件:两台API运行级别在24或更高版本的Android设备并装有谷歌商店。

AR设备支持了解对设备的详细要求

注意:尽管你不需要Java/Android的相关知识也能部分理解教程,但在此过程中我们也不会对 Java/ARCore 的某些概念进行过多额外解释。

总览

在本指南中,我们将构建一个支持AR功能的客户支持应用程序。 其中一个用户可以通过输入频道名称来创建频道,并以主播(需要支持的人)的身份加入该频道。 而另一个用户可以通过输入相同的频道名称,以观众(客户支持)的身份来加入同一频道。 当两个用户都在频道中时,主播可以向观众推送他们后置摄像头所拍摄的画面。 观众可以在自己的设备上绘图,而这些触摸输入会在主播端通过增强现实技术渲染出来!
在本章中我们会主要讲以下几步骤

1.创建新项目

2.创建UI

3.开启AR Core

4.开启主播语音视频

5.开启观众语音视频

6.远程协助功能

7.建立并检测设备

您可以通过我的Github找到相关 demo app 作为本文的参考。

创建新项目

首先,我们需开启Andnroid Studio新建一个空白项目。

1.打开Android Studio并点击 开启一个新的Android Studio项目

2.在“ 选择项目 ”面板上,选择“ 手机和平板电脑 ”>“ 创建空的Activity ”,然后单击“ 下一步 ”。

3.单击 “完成”. 如果您需要安装任何插件,请按照屏幕上的说明进行操作。

集成SDK

在您所创建项目的 /app/build.gradle 文件中添加以下行:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    
    //ARCore 
    implementation 'com.google.ar:core:1.0.0'
    implementation 'de.javagl:obj:0.2.1'
    implementation 'com.google.android.material:material:1.1.0'
    implementation 'com.android.support:appcompat-v7:27.0.2'
    implementation 'com.android.support:design:27.0.2'

    //Video 
    implementation 'io.agora.rtc:full-sdk:3.1.3'
}

更改后同步项目。 在**/app/src/main/AndroidManifest.xml**文件中添加以下项目权限:

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.BLUETOOTH" />

为了运行ARCore,我们还需要在AndroidManifest.xml文件中添加以下内容。 这表明该应用程序需要ARCore。

<uses-feature
    android:name="android.hardware.camera.ar"
    android:required="true" />
<application
...
<meta-data
    android:name="com.google.ar.core"
    android:value="required" />
</application>

创建UI

让我们为两个用户创建UI组件。 对于用AR技术进行远程共享的用户,我们将其称为“主播”。对于其他加入频道提供支持的用户,我们将其称为“观众”。 以下是我将使用的UI示例:


主播 UI (左) 观众UI (右)

请注意:您可以在 GitHub仓库 找到UI的.xml文件。

主播的UI界面和观众UI界面之间的主要不同在于主播UI界面所使用的是 android.opengl.GLSurfaceView 来渲染AR相机视图,而观众UI界面是通过 RelativeLayout 来渲染从主播那里得到的AR相机视图

主播用户界面右上角的容器用来渲染观众本地相机的远程视频。 观众用户界面右上角的容器用来呈现观众的本地相机视图。

我们还将创建一个屏幕,供用户输入频道名称并以主播或观众的身份加入频道。


Join Channel UI

请注意:您可以在GitHub仓库找到加入频道UI的.xml文件。

Channel UI下的逻辑非常简单。 如果用户单击“创建”按钮,它将跳至处理主播逻辑的主播Activity页面。 如果用户单击“ 加入 ”按钮,它将跳至处理观众互动逻辑的观众活动页面。 现在,我们需要为这两个活动定义逻辑。

创建ARCore

在项目内创建ARCore并不像我们想象中的那么难。在主播Activity的 OnResume() 方法中,我们需要创建一个Session实例。 Session实例用于管理AR系统状态并处理生命周期。 我们可以用它来接收允许访问相机图像的帧。 但是,在此之前,我们必须检查是否已安装ARCore。

@Override
protected void onResume() {
    super.onResume();

    if (mSession == null) {
        String message = null;
        try {
            switch (ArCoreApk.getInstance().requestInstall(this, !installRequested)) {
                case INSTALL_REQUESTED:
                    installRequested = true;
                    return;
                case INSTALLED:
                    break;
            }

            // ARCore requires camera permissions to operate. If we did not yet obtain runtime permission on Android M and above, now is a good time to ask the user for it.
            if (!CameraPermissionHelper.hasCameraPermission(this)) {
               CameraPermissionHelper.requestCameraPermission(this);
                return;
            }

            mSession = new Session(this);
        } catch (Exception e) {
            ...
        } 
        // Create default config and check if supported.
        Config config = new Config(mSession);
        if (!mSession.isSupported(config)) {
            showSnackbarMessage("This device does not support AR", true);
        }
        ...
}

实现GLSurfaceView.Renderer

我们将使用GLSurfaceView渲染AR相机。 为此,主播Activity应实现 GLSurfaceView.Renderer 。 需要重写三个函数 :分别是onSurfaceCreated,onSurfaceChanged和onDrawFrame

覆盖onSurfaceCreated

通常在开始渲染时使用的调用onSurfaceCreated方法中,我们需要对AR场景进行一些初始化设置。

1.创建一个BackgroundRenderer实例,并将其ID传输给会话相机。

2.初始化3D对象。 稍后,将3D对象在主播的AR世界中渲染。

3.初始化平面检测渲染。

4.初始化点云。

@Override
**public void** onSurfaceCreated(GL10 gl, EGLConfig config) {
GLES20. *glClearColor* (0.1f,0.1f,0.1f,1.0f);

<em>// Create the texture and pass it to ARCore session to be filled during update().</em>
**mBackgroundRenderer** .createOnGlThread( */*context=*/* **this** );
**if** ( **mSession** != **null** ) {
**mSession** .setCameraTextureName( **mBackgroundRenderer** .getTextureId());
} <em>// Prepare the other rendering objects.</em>
**try** {
**mVirtualObject** .createOnGlThread( */*context=*/* **this** , **"andy.obj"** , **"andy.png"** );
**mVirtualObject** .setMaterialProperties(0.0f, 3.5f, 1.0f, 6.0f);
**mVirtualObjectShadow** .createOnGlThread( */*context=*/* **this** ,
**"andy_shadow.obj"** , **"andy_shadow.png"** );
**mVirtualObjectShadow** .setBlendMode(ObjectRenderer.BlendMode. ***Shadow*** );
**mVirtualObjectShadow** .setMaterialProperties(1.0f, 0.0f, 0.0f, 1.0f);
} **catch** (IOException e) {
...
}
**try** {
**mPlaneRenderer** .createOnGlThread( */*context=*/* **this** , **"trigrid.png"** );
} **catch** (IOException e) {
...
}
**mPointCloud** .createOnGlThread( */*context=*/* **this** );
}

重写onSurfaceChanged

我们会在onSurfaceChanged方法中设置viewport,该方法会在完成Surface创建后、以及Surface尺寸发生改变的时候被调用。

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
    ...
    GLES20.glViewport(0, 0, width, height);
}

覆盖onDrawFrame

在用于绘制当前帧的onDrawFrame方法中,我们需要为GLSurfaceView实现渲染逻辑。

  1. 清屏

2.从ARSession获取最新的帧。

3.捕获用户的点击并检查是否在场景中找到了任何平面。 如果是这样,请在该点创建一个锚点。

4.绘制背景。

5.绘制点云。

  1. 迭代所有锚点,并在每个锚点上绘制3D对象。
@Override
public void onDrawFrame(GL10 gl) {
    // Clear screen to notify driver it should not load any pixels from previous frame.
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
    ...
    
    try {
        // Obtain the current frame from ARSession. When the configuration is set to
        // UpdateMode.BLOCKING (it is by default), this will throttle the rendering to the camera framerate.
        Frame frame = mSession.update();
        Camera camera = frame.getCamera();

        // Handle taps. Handling only one tap per frame, as taps are usually low frequency
        // compared to frame rate.
        MotionEvent tap = queuedSingleTaps.poll();
        if (tap != null && camera.getTrackingState() == TrackingState.TRACKING) {
            for (HitResult hit : frame.hitTest(tap)) {
                // Check if any plane was hit, and if it was hit inside the plane polygon
                Trackable trackable = hit.getTrackable();
                // Creates an anchor if a plane or an oriented point was hit.
                if ((trackable instanceof Plane && ((Plane) trackable).isPoseInPolygon(hit.getHitPose()))
                        || (trackable instanceof Point
                        && ((Point) trackable).getOrientationMode()
                        == Point.OrientationMode.ESTIMATED_SURFACE_NORMAL)) {
                    // Hits are sorted by depth. Consider only closest hit on a plane or oriented point.
                    // Cap the number of objects created. This avoids overloading both the
                    // rendering system and ARCore.
                    if (anchors.size() >= 250) {
                        anchors.get(0).detach();
                        anchors.remove(0);
                    }
                    // Adding an Anchor tells ARCore that it should track this position in
                    // space. This anchor is created on the Plane to place the 3D model
                    // in the correct position relative both to the world and to the plane.
                    anchors.add(hit.createAnchor());
                    break;
                }
            }
        }

        // Draw background.
        mBackgroundRenderer.draw(frame);

        ...
        if (isShowPointCloud()) {
            // Visualize tracked points.
            PointCloud pointCloud = frame.acquirePointCloud();
            mPointCloud.update(pointCloud);
            mPointCloud.draw(viewmtx, projmtx);

            // Application is responsible for releasing the point cloud resources after
            // using it.
            pointCloud.release();
        }

        ...

        if (isShowPlane()) {
            // Visualize planes.
            mPlaneRenderer.drawPlanes(
                    mSession.getAllTrackables(Plane.class), camera.getDisplayOrientedPose(), projmtx);
        }

        // Visualize anchors created by touch.
        float scaleFactor = 1.0f;

        for (Anchor anchor : anchors) {
            if (anchor.getTrackingState() != TrackingState.TRACKING) {
                continue;
            }
            // Get the current pose of an Anchor in world space. The Anchor pose is updated
            // during calls to session.update() as ARCore refines its estimate of the world.
            anchor.getPose().toMatrix(mAnchorMatrix, 0);


            // Update and draw the model and its shadow.
            mVirtualObject.updateModelMatrix(mAnchorMatrix, mScaleFactor);
            mVirtualObjectShadow.updateModelMatrix(mAnchorMatrix, scaleFactor);
            mVirtualObject.draw(viewmtx, projmtx, lightIntensity);
            mVirtualObjectShadow.draw(viewmtx, projmtx, lightIntensity);
        }
    } catch (Throwable t) {
        ...
    }
}

注意:一些概念并不出自这里。请点击 Github code 以进行更深入的了解。

Enable Streamer’s Video Call

启用主播的视频通话

设置来自主播的视频通话

在AgoraARStreamerActivity的**onCreate()**方法中,执行以下操作:

1.初始化GLSurfaceView onTouchListener

2.初始化Agora RtcEngine

3.设置自定义视频源

4.加入频道

1.初始化GLSurfaceView onTouchListener

为GLSurfaceView设置onTouchListener,允许我们捕获接触点并在该位置设置AR锚点。

mGestureDetector = new GestureDetector(this,
        new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                onSingleTap(e);
                return true;
            }

            @Override
            public boolean onDown(MotionEvent e) {
                return true;
            }
        });

mSurfaceView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        return mGestureDetector.onTouchEvent(event);
    }
});

当GestureDetector检测到单击时,它将触发onSingleTap。 在这种方法中,我们会将这次单击添加到single tap队列中。

private void onSingleTap(MotionEvent e) {
    queuedSingleTaps.offer(e);
}

还记得我们在onDrawFrame方法中创建锚点吗? 我们从 queuedSingleTaps .中轮询了用户的点击。 并将单击添加到队列的位置。

2.初始化Agora RtcEngine

为了初始化Agora视频引擎,只需简单地调用**RtcEngine.create(context,appid,RtcEventHandler)**创建一个RtcEngine实例就好了。

mRtcEngine = RtcEngine.create(this, getString(R.string.private_broadcasting_app_id), mRtcEventHandler);

为了在参数中获取应用App ID,请按照下列步骤操作:

1.在Agora控制台中创建一个Agora项目

2.单击左侧导航面板上的“项目管理”选项卡。

3.单击“创建”,然后按照屏幕上的说明设置项目名称,选择身份验证机制,然后单击“提交”。

4.在“项目管理”页面上,找到你的App ID。

mRtcEventHandler是用于管理RtcEngine发生的不同事件的处理程序。 让我们用一些基本事件处理程序来实现它。

private IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {
    @Override
    public void onJoinChannelSuccess(final String channel, int uid, int elapsed) {
        //when local user joined the channel
        ...
    }

    @Override
    public void onRemoteVideoStateChanged(final int uid, int state, int reason, int elapsed) {
        super.onRemoteVideoStateChanged(uid, state, reason, elapsed);
        //when remote user join the channel
        if (state == Constants.REMOTE_VIDEO_STATE_STARTING) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    addRemoteRender(uid);
                }
            });
        }
    }

    @Override
    public void onUserOffline(int uid, int reason) {
        //when remote user leave the channel
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                removeRemoteRender();
            }
        });
    }

    @Override
    public void onStreamMessage(int uid, int streamId, byte[] data) {
        //when received the remote user's stream message data
        ...
    }
};

检查每个事件处理程序方法顶部的注释,以便更好地理解它们。 有关你能使用的更多RtcEngine事件处理程序,请查看Agora Rtc API文档

请注意:屏幕上显示视频视图的某些逻辑已隐藏。 您可以查看Github demo app,以更好地了解如何在屏幕上动态显示和删除视频视图。

3.设置自定义视频源

在应用程序中,我们想让流媒体将其AR世界发送给观众。 因此,它们发送的视频来自自定义视频源,而不是一般的相机视频流。 幸运的是,Agora Video SDK提供了API方法来发送自定义视频源。

创建一个名为AgoraVideoSource并实现IVideoSource接口的类。 IVideoSource接口定义了一组协议以实现自定义视频源,并将其传递给底层媒体引擎以替换默认视频源。

public class AgoraVideoSource implements IVideoSource {
    private IVideoFrameConsumer mConsumer;

    @Override
    public boolean onInitialize(IVideoFrameConsumer iVideoFrameConsumer) {
        mConsumer = iVideoFrameConsumer;
        return true;
    }

    @Override
    public boolean onStart() {
        return true;
    }

    @Override
    public void onStop() {
    }

    @Override
    public void onDispose() {
    }

    @Override
    public int getBufferType() {
        return MediaIO.BufferType.BYTE_ARRAY.intValue();
    }

    public IVideoFrameConsumer getConsumer() {
        return mConsumer;
    }
}

在AgoraARStreamerActivity内的onCreate方法中,调用构造函数方法以创建AgoraVideoSource实例。

mSource = new AgoraVideoSource();

创建一个名为AgoraVideoRender并实现IVideoSink接口的类。 IVideoSink接口定义了一组协议以创建自定义的视频接收器并将其传递给媒体引擎以替换默认的视频渲染器。

public class AgoraVideoRender implements IVideoSink {
    private Peer mPeer;
    private boolean mIsLocal;

    public AgoraVideoRender(int uid, boolean local) {
        mPeer = new Peer();
        mPeer.uid = uid;
        mIsLocal = local;
    }

    public Peer getPeer() {
        return mPeer;
    }

    @Override
    public boolean onInitialize() {
        return true;
    }

    @Override
    public boolean onStart() {
        return true;
    }

    @Override
    public void onStop() {

    }

    @Override
    public void onDispose() {

    }

    @Override
    public long getEGLContextHandle() {
        return 0;
    }

    @Override
    public int getBufferType() {
        return MediaIO.BufferType.BYTE_BUFFER.intValue();
    }

    @Override
    public int getPixelFormat() {
        return MediaIO.PixelFormat.RGBA.intValue();
    }

    @Override
    public void consumeByteBufferFrame(ByteBuffer buffer, int format, int width, int height, int rotation, long ts) {
        if (!mIsLocal) {
            mPeer.data = buffer;
            mPeer.width = width;
            mPeer.height = height;
            mPeer.rotation = rotation;
            mPeer.ts = ts;
        }
    }

    @Override
    public void consumeByteArrayFrame(byte[] data, int format, int width, int height, int rotation, long ts) {
        
    }

    @Override
    public void consumeTextureFrame(int texId, int format, int width, int height, int rotation, long ts, float[] matrix) {

    }

}

与AgoraVideoSource实例类似,我们通过调用其构造函数来创建AgoraVideoRender实例。 在这里,我们将uid设为0来表示本地视频渲染。

mRender = new AgoraVideoRender(0, true);

创建两个实例后,我们需要设置自定义AR视频源和本地视频渲染器。

mRtcEngine.setVideoSource(mSource);
mRtcEngine.setLocalVideoRenderer(mRender);

但是,我们设置的视频源中没有数据。 我们需要将AR相机视图传到我们的视频源。 为此,我们将在之前重写的onDrawFrame方法的末尾添加逻辑。

@Override
public void onDrawFrame(GL10 gl) {
    ...
    final Bitmap outBitmap = Bitmap.createBitmap(mSurfaceView.getWidth(), mSurfaceView.getHeight(), Bitmap.Config.ARGB_8888);
    PixelCopy.request(mSurfaceView, outBitmap, new PixelCopy.OnPixelCopyFinishedListener() {
        @Override
        public void onPixelCopyFinished(int copyResult) {
            if (copyResult == PixelCopy.SUCCESS) {
                sendARView(outBitmap);
            } else {
                Toast.makeText(AgoraARCoreActivity.this, "Pixel Copy Failed", Toast.LENGTH_SHORT);
            }
        }
    }, mSenderHandler);
}
private void sendARView(Bitmap bitmap) {
    if (bitmap == null) return;

    if (mSource.getConsumer() == null) return;

    //Bitmap bitmap = source.copy(Bitmap.Config.ARGB_8888,true);
    int width = bitmap.getWidth();
    int height = bitmap.getHeight();

    int size = bitmap.getRowBytes() * bitmap.getHeight();
    ByteBuffer byteBuffer = ByteBuffer.allocate(size);
    bitmap.copyPixelsToBuffer(byteBuffer);
    byte[] data = byteBuffer.array();

    mSource.getConsumer().consumeByteArrayFrame(data, MediaIO.PixelFormat.RGBA.intValue(), width, height, 0, System.currentTimeMillis());
}

这里的逻辑是将GLSurfaceView复制到点阵图,并将点阵图缓冲区发送到我们的自定义视频源。

4.加入频道

现在,我们准备通过传递上一个Activity的channelName来调用RtcEngine实例上的**joinChannel()**来加入频道。

mRtcEngine.joinChannel(null, channelName, "", 0);

注意:参数中的token可以设置为null。

通过调用此函数并成功加入频道,RtcEngineEventHandler将触发我们在上一步中完成的**onJoinChannelSuccess()**方法。 它将返回由Agora服务器生成的唯一的Agora视频ID。

至此,主播可以加入频道并向观众宣传他们的AR世界。

启用观众的视频通话

启用观众的视频通话与我们为主播编写的视频通话非常相似。 唯一的区别是在初始化RtcEngine之后设置本地相机视频。

mRtcEngine.enableVideo();
mLocalView = RtcEngine.CreateRendererView(getBaseContext());
mLocalContainer.addView(mLocalView);
mLocalView.setZOrderMediaOverlay(true);

VideoCanvas localVideoCanvas = new VideoCanvas(mLocalView, VideoCanvas.RENDER_MODE_HIDDEN, 0);
mRtcEngine.setupLocalVideo(localVideoCanvas);

这会将观众的本地相机视图显示在屏幕的右上角。

注意:想要更深入理解,这里可参考Github代码。

现在,我们可以在主播和观众之间发起视频通话。 但是,这个远程协助应用程序功能仍不齐全,因为观众无法与流媒体的AR世界互动。 接下来,我们将开始通过ARCore实现观众标记功能。

远程协助功能

理想情况下,当远程技术人员(观众)想通过在屏幕上绘图来指导客户(主播)时,这个功能应能提供帮助。 该标记应立即在客户端显示,并应保持与绘制位置相同的位置。

为了实现这一点,我们将收集观众的接触点,并将其发送给主播。 只要主播接收到这些触摸点,我们就可以在流光的屏幕上模拟触摸以创建AR对象。

我们首先要收集观众的触摸位置。 在AgoraARAudienceActivity的onCreate方法中,在远程视图容器上设置触摸侦测器。 收集接近屏幕中心的所有接触点位置。 使用Agora API方法sendStreamMessage将它们作为数据流消息发送到主播。 这将触发主播的onStreamMessage回调。 由于用户每秒只能发送6kB数据,因此只要我们收集了10个接触点,我们就会将其发送出去。

mRemoteContainer.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                //get the touch position related to the center of the screen
                touchCount++;
                float x = event.getRawX() - ((float)mWidth / 2);
                float y = event.getRawY() - ((float)mHeight / 2);
                floatList.add(x);
                floatList.add(y);
                if (touchCount == 10) {
                    //send the touch positions when collected 10 touch points
                    sendMessage(touchCount, floatList);
                    touchCount = 0;
                    floatList.clear();
                }
                break;
            case MotionEvent.ACTION_UP:
                //send touch positions after the touch motion
                sendMessage(touchCount, floatList);
                touchCount = 0;
                floatList.clear();
                break;
        }
        return true;
    }
});

这是sendMessage逻辑:

/**
 * send the touch points as a byte array to Agora sdk
 * @param touchCount
 * @param floatList
 */
private void sendMessage(int touchCount, List<Float> floatList) {
    byte[] motionByteArray = new byte[touchCount * 4 * 2];
    for (int i = 0; i < floatList.size(); i++) {
        byte[] curr = ByteBuffer.allocate(4).putFloat(floatList.get(i)).array();
        for (int j = 0; j < 4; j++) {
            motionByteArray[i * 4 + j] = curr[j];
        }
    }
    mRtcEngine.sendStreamMessage(dataChannel, motionByteArray);
}

现在,在AgoraARStreamerActivity中,我们需要覆盖onStreamMessage回调,以接收观众发送的接触点并模拟主播屏幕上的触摸操作。

@Override
public void onStreamMessage(int uid, int streamId, byte[] data) {
    //when received the remote user's stream message data
    super.onStreamMessage(uid, streamId, data);
    int touchCount = data.length / 8;       //number of touch points from data array
    for (int k = 0; k < touchCount; k++) {
        //get the touch point's x,y position related to the center of the screen and calculated the raw position
        byte[] xByte = new byte[4];
        byte[] yByte = new byte[4];
        for (int i = 0; i < 4; i++) {
            xByte[i] = data[i + 8 * k];
            yByte[i] = data[i + 8 * k + 4];
        }
        float convertedX = ByteBuffer.wrap(xByte).getFloat();
        float convertedY = ByteBuffer.wrap(yByte).getFloat();
        float center_X = convertedX + ((float) mWidth / 2);
        float center_Y = convertedY + ((float) mHeight / 2);

        //simulate the clicks based on the touch position got from the data array
        instrumentation.sendPointerSync(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, center_X, center_Y, 0));
        instrumentation.sendPointerSync(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, center_X, center_Y, 0));
    }
}

在设备上构建和测试

这就是构建远程协助应用程序所需要的全部程序。 现在就可以运行我们的程序啦!

转到Android Studio,确保已插入您的Android设备,然后单击“运行”在设备上构建应用程序。别忘了两个设备上都要构建应用程序才能开始视频通话。 两种设备都必须运行Android API级别24或更高版本,并且最初都自带Google Play商店。

您可以在AR支持设备查看详细的设备要求。

作者 杨少成

推荐阅读
作者信息
AgoraTechnicalTeam
TA 暂未填写个人简介
文章
121
相关专栏
SDK 教程
55 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和 Agora 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。