Agora 视频通话和互动白板

under Android  agora  tag     Published on July 6th , 2021 at 11:42 am

官网快速实现视频通话
官网互动白板加入实时房间

跟着官网里的内容走不会出错,本篇我把这两个功能放一起,实现简单且功能不多的灵动课堂。官网有灵动课堂演示 App 可以下载看看。

十分简易版灵动课堂

surface_agora_01.jpg

认真阅读官方文档!

activity_main.xml

右边两个 Fragment 是视频部分,左边 TextView 是白板功能,WhiteboardView 实现白板。白板功能太多,贴出来的代码省略了一点

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".agora.whiteboard.WhiteBoardActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <com.herewhite.sdk.WhiteboardView
            android:id="@+id/white"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="2"
            android:visibility="visible" />

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:orientation="vertical">

            <androidx.constraintlayout.widget.ConstraintLayout
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1">

                <FrameLayout
                    android:id="@+id/fl_local"
                    android:layout_width="match_parent"
                    android:layout_height="0dp"
                    android:layout_weight="1"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toTopOf="parent" />

                <TextView
                    android:id="@+id/tv_micro_local"
                    style="@style/text_padding5_black_center"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:background="@color/teal_200"
                    android:text="麦克风"
                    app:layout_constraintBottom_toBottomOf="parent"
                    app:layout_constraintStart_toStartOf="parent" />
            </androidx.constraintlayout.widget.ConstraintLayout>

            <androidx.constraintlayout.widget.ConstraintLayout
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1">

                <FrameLayout
                    android:id="@+id/fl_rem"
                    android:layout_width="match_parent"
                    android:layout_height="0dp"
                    android:layout_weight="1"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toTopOf="parent" />
            </androidx.constraintlayout.widget.ConstraintLayout>

        </LinearLayout>
    </LinearLayout>

    <androidx.core.widget.NestedScrollView
        android:id="@+id/nsv"
        android:layout_width="150dp"
        android:layout_height="300dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
            <!-- 这部分是左边白板功能 -->
            
            <TextView
                android:id="@+id/clear"
                style="@style/text_padding5_black_center"
                android:layout_width="100dp"
                android:layout_height="wrap_content"
                android:onClick="clear"
                android:text="清屏(保留 ppt)"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <TextView
                android:id="@+id/clear_ppt"
                style="@style/text_padding5_black_center"
                android:layout_width="120dp"
                android:layout_height="wrap_content"
                android:onClick="clearPPT"
                android:text="清屏(不保留 ppt)"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/clear" />
                
            <!-- 这部分是左边白板功能 -->
        </LinearLayout>
    </androidx.core.widget.NestedScrollView>

    <TextView
        android:id="@+id/leave"
        style="@style/text_padding5_black_center"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:text="离开房间"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

视频通话

视频通话的代码不贴太多!没有仔细研究,直接复制粘贴官网的

视频通话需要注意的是进入频道使用的 uid

如果想要从不同的设备同时接入同一个频道,请确保每个设备上使用的 UID 是不同的

IRtcEngineEventHandler 接口类

用于 SDK 向 App 发送回调事件通知,App 通过继承该接口类的方法获取 SDK 的事件通知。

  • onJoinChannelSuccess(String channel, final int uid, int elapsed)

    本地用户成功加入频道时,会触发该回调。

  • onUserJoined(final int uid, int elapsed)

    远端用户成功加入频道时,会触发该回调。

// 可以在该回调中调用 setupRemoteVideo 方法设置远端视图。
public void onUserJoined(final int uid, int elapsed) {
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            Log.i("agora", "远端用户上线: " + (uid & 0xFFFFFFFFL));
            setupRemoteVideo(uid);
        }
    });
}

private void setupRemoteVideo(int uid) {
    // 创建一个 SurfaceView 对象。
    SurfaceView mRemoteView = RtcEngine.CreateRendererView(getBaseContext());
    mRemoteContainer.addView(mRemoteView);
    // 设置远端视图。
    mRtcEngine.setupRemoteVideo(new VideoCanvas(mRemoteView, VideoCanvas.RENDER_MODE_HIDDEN, uid));
}

<p style="color:red">

</p>

VideoEncoderConfiguration 视频编码属性的定义。

需要在加入频道前设置

private void initEngineAndJoinChannel() {
    initializeEngine(); // 初始化 mRtcEngine
    setupVideoConfig(); // 设置视频编码属性
    setupLocalVideo(); // 设置本地视图
    joinChannel(); // 加入频道
}

private void setupVideoConfig() {
    mRtcEngine.setVideoEncoderConfiguration(new VideoEncoderConfiguration(
            VideoEncoderConfiguration.VD_1280x720, // 视频分辨率
            VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_30, // 视频编码的帧率(fps),默认值为 15。
            VideoEncoderConfiguration.STANDARD_BITRATE, // 标准码率模式。该模式下,视频在通信和直播场景下的码率有所不同
            VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_PORTRAIT)); // 视频编码的方向模式。该模式下 SDK 固定输出人像(竖屏)模式的视频,如果采集到的视频是横屏模式,则视频编码器会对其进行裁剪。
}

互动白板

官网白板部分乱乱的,直接拿官网代码说吧

生成 Room Token

生成各种 Token:https://docs.agora.io/cn/whiteboard/generate_whiteboard_token?platform=RESTful ,这个详细很多。

lifespan: Token 的有效时间(ms)。设为 0 表示永久有效。并不推荐设置永久有效,不安全。如果设置 Token 不是永久有效一定要注意使用时 Token 是否过期!!
role: 权限角色。admin、writer、reader,详情见 Token 类型与权限

Postman 生成

版本

为什么改使用版本呢,因为官网有句设置数据中心的代码我没找到(不写连接不到房间),改了下面的版本就有了。

implementation 'com.github.duty-os:white-sdk-android:2.13.4'

MainActivity.java

调整了 sdkConfiguration.setRegion(Region.cn); 位置

Object o = t.getMessage();
Log.i("showToast", o.toString());
无故闪退一定要看看这条打印了什么日志!很可能是 Token 过期了!

package com.example.whiteboard;
import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;

import com.herewhite.sdk.RoomParams;
import com.herewhite.sdk.WhiteboardView;
import com.herewhite.sdk.WhiteSdk;
import com.herewhite.sdk.WhiteSdkConfiguration;
import com.herewhite.sdk.Room;
import com.herewhite.sdk.domain.Promise;

import com.herewhite.sdk.domain.SDKError;
import com.herewhite.sdk.domain.MemberState;


public class MainActivity extends AppCompatActivity {

    // 你的互动白板 App Identifier
    final String appId = "Your App Identifier";
    // 你的房间 UUID
    final String uuid = "房间 UUID";
    // 你的 Room Token
    final String roomToken = "Room Token";
    // 创建 WhiteboardView 对象,实现白板 view
    WhiteboardView whiteboardView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 创建 WhiteSdkConfiguration 对象,设置白板的 App Identifier 和日志参数
        WhiteSdkConfiguration sdkConfiguration = new WhiteSdkConfiguration(appId, true);
        // 设置数据中心为中国杭州
        sdkConfiguration.setRegion(Region.cn);
        // 创建 RoomParams 对象,设置房间参数,用于加入房间
        RoomParams roomParams = new RoomParams(uuid, roomToken);
        // 将 layout 中的白板 view 赋值给 WhiteboardView 对象
        whiteboardView = findViewById(R.id.white);
        // 创建 WhiteSdk 对象,用于初始化白板 SDK
        WhiteSdk whiteSdk = new WhiteSdk(whiteboardView, MainActivity.this, sdkConfiguration);
        // 加入房间
        whiteSdk.joinRoom(roomParams, new Promise<Room>() {
            @Override
            public void then(Room wRoom) {
                MemberState memberState = new MemberState();
                // 将教具设置为铅笔
                memberState.setCurrentApplianceName("pencil");
                // 将颜色设置为红色
                memberState.setStrokeColor(new int[]{255, 0, 0});
                // 设置当前用户教具
                wRoom.setMemberState(memberState);
            }

            @Override
            public void catchEx(SDKError t) {
                Object o = t.getMessage();
                Log.i("showToast", o.toString());
                Toast.makeText(MainActivity.this, o.toString(), Toast.LENGTH_SHORT).show();
            }
        });

    }

    @Override
    protected void onDestroy() {
            super.onDestroy();
            whiteboardView.removeAllViews();
            whiteboardView.destroy();
        }
    }

操作房间

主要涉及两个类:

  • MemberState 类,用于设置互动白板实时房间的白板工具状态。
  • Room 类,用于操作互动白板实时房间。

MemberState

Appliance:白板工具名称,例如:橡皮工具、文本输入框等。ctrl 进入类查看或者自行在官方文档中搜索 Appliance

MemberState memberState = new MemberState();
// 设置线条颜色,为 RGB 格式,例如,[0, 0, 255] 表示蓝色。
memberState.setStrokeColor(strokeColor);
// 设置线条粗细
memberState.setStrokeWidth(strokeWidth);
// 设置字体大小。Chrome 浏览器对于小于 12 的字体会自动调整为 12。
memberState.setTextSize(10);
// 设置互动白板实时房间内使用的白板工具,默认是 PENCIL
memberState.setCurrentApplianceName(Appliance.PENCIL);
room.setMemberState(memberState);

Room

Room 内方法较多,想了解更多请 ctrl 进入类查看方法说明或者自行在官方文档中搜索 Room

// 修改房间内的白板工具状态
room.setMemberState(memberState);
// 是否禁止本地序列化:`true`:(默认)禁止开启本地序列化;`false`: 开启本地序列化,即可以对本地操作进行解析。
room.disableSerialization(false);
// 撤销上一步操作。该方法仅当 disableSerialization 设为 `false` 时生效。
room.undo();
// 清除当前场景的所有内容。`true`:保留 PPT。`false`:连 PPT 一起清空。
room.cleanScene(true);

// 插入空白页。第一个参数场景组,必须以 `/` 开头。不能为场景路径。
room.putScenes("/dir", new Scene[]{new Scene("page1")}, 0);
// 切换至指定的场景。想要切换到的场景的场景路径,请确保场景路径以 `/` 开头,由场景组和场景名构成,例如,`/math/classA`.
room.setScenePath("/dir" + "/page1");

// 获取房间的用户列表。
room.getRoomMembers(new Promise<RoomMember[]>() {
    @Override
    public void then(RoomMember[] roomMembers) {
        // 房间的用户列表仅包含互动模式(具有读写权限)的用户,不包含订阅模式(只读权限)的用户。
    }

    @Override
    public void catchEx(SDKError t) {
        Object o = t.getMessage();
        Log.i("showToast", o.toString());
    }
});

// 主播模式
// 房间内其他人的视角模式会被自动修改成 follower,并且强制观看你的视角。
// 如果房间内存在另一个主播,该主播的视角模式也会被强制改成 follower。
// 就好像你抢了他/她的主播位置一样。
room.setViewMode(ViewMode.Broadcaster);

// 自由模式
// 你可以自由放缩、移动视角。
// 即便房间里有主播,主播也无法影响你的视角。
room.setViewMode(ViewMode.Freedom);

// 追随模式
// 你将追随主播的视角。主播在看哪里,你就会跟着看哪里。
// 在这种模式中如果你放缩、移动视角,将自动切回 freedom模式。
room.setViewMode(ViewMode.Follower);

onDestroy()

一定要写!不知道不写会不会收费!

@Override
protected void onDestroy() {
    super.onDestroy();
    whiteboardView.removeAllViews();
    whiteboardView.destroy();

    leaveChannel();
    RtcEngine.destroy();
}

关于白板截图录制(实际是创建房间)PPT转换场景管理等需要转到 netless 查看文档

截图并插入到指定区域展示图片

标题截图中附上官方文档链接了,图是文档里截的文档里截的文档里截的,能看文档的尽量看文档!

截图成功返回的数据,插入图片需要 url

public void screenshots() {
    JSONObject object = new JSONObject();
    try {
        object.put("width", 200);
        object.put("height", 100);
        MediaType JSON = MediaType.parse("application/json; charset=utf-8");
        RequestBody body = RequestBody.create(JSON, object.toString());
        OkGo.<String>post("https://api.netless.link/v5/rooms/" + getString(R.string.room_uuid) + "/screenshots")
                .tag(this)
                .headers("token", getString(R.string.room_token))
                .headers("region", "cn-hz")
                .params("uuid", getString(R.string.room_uuid))
                .upRequestBody(body)
                .execute(new StringCallback() {
                    @Override
                    public void onSuccess(Response<String> response) {
                        String json = response.body();
                        if (json.contains("url")) {
                            try {
                                JSONObject jsonObject = new JSONObject(json);
                                insertImage(jsonObject.getString("url"));
                            } catch (JSONException e) {
                                e.printStackTrace();
                            }
                        }
                    }

                    @Override
                    public void onError(Response<String> response) {
                        super.onError(response);
                    }
                });
    } catch (JSONException e) {
        e.printStackTrace();
    }
}

// 插入并展示图片
private void insertImage(String url) {
    room.insertImage(new ImageInformationWithUrl(0d, 0d, 200d, 100d, url));
}

录制(实际是创建房间)

isRecord 不是必填项, Agora 白板没有对此说明(可能说了但是我没找到)。默认是 false,不开启。

插一嘴:limit,进入房间人数限制。场景:灵动课堂一对一、一对多。

开启需谨慎!要收费!

场景管理

刚开始直接看代码里的方法说明,对‘场景’很不理解,看完文档瞬间懂了,文档很详细!看文档!

了解场景的主要目的是想做用户定位到哪一页和白板总数的指引(‘1/2’),不说做的高大上,重要的数据总要知道怎么获得

主要类(这个类是不用自己写的):

  • SceneState:场景状态

方法说明里的注释还是很贴心的!加一点自己的理解

SceneState.java

package com.herewhite.sdk.domain;

/**
 * 场景状态。
 */
public class SceneState extends WhiteObject {

    private Scene[] scenes; // scenes.length 当前场景组中场景的总数。room.getScenes().length 也可以获得总数
    private String scenePath; // 比如打印的是 "/math/page1",其中 "/math" 是场景组
    private int index; // 当前场景在所属场景组中的索引号。从 0 开始
    
    // 删了,理解了场景组和场景,对 SceneState 中的方法也都能理解
}

切换上一页、下一页场景

public void nextScene() {
    int nextIndex = mRoom.getSceneState().getIndex() + 1;
    if (mRoom.getScenes().length == nextIndex) {
        Toast.makeText(this, "已经是最后一页了", Toast.LENGTH_SHORT).show();
        return;
    }
    mRoom.setSceneIndex(nextIndex, new Promise<Boolean>() {
        @Override
        public void then(Boolean result) {
            changeIndex(mRoom.getSceneState().getIndex() + 1, mRoom.getScenes().length);
        }

        @Override
        public void catchEx(SDKError t) {
            // 打印错误信息
        }
    });
}

public void upScene() {
    if (mRoom.getSceneState().getIndex() == 0) {
        Toast.makeText(this, "已经在第一页了", Toast.LENGTH_SHORT).show();
        return;
    }
    int upIndex = mRoom.getSceneState().getIndex() - 1;
    mRoom.setSceneIndex(upIndex, new Promise<Boolean>() {
        @Override
        public void then(Boolean result) {
            changeIndex(mRoom.getSceneState().getIndex() + 1, mRoom.getScenes().length);
        }

        @Override
        public void catchEx(SDKError t) {
            // 打印错误信息
        }
    });
}

本文由 surface 创作,采用 知识共享署名4.0 国际许可协议进行许可,转载前请务必署名
  文章最后更新时间为:August 11th , 2021 at 10:32 am
分享到:Twitter  Weibo  Facebook