跟着官网里的内容走不会出错,本篇我把这两个功能放一起,实现简单且功能不多的灵动课堂。官网有灵动课堂演示 App 可以下载看看。
十分简易版灵动课堂
认真阅读官方文档!
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) {
// 打印错误信息
}
});
}