使用声网&环信 SDK 构建元宇宙应用 MetaTown 的最佳实践|2022 RTE 编程挑战赛
参加了【RTE2022 创新编程挑战赛】,整个大赛历时47天,参赛者除了对声网的 SDK 有了很多的体验,还做了很多奇思妙想的结合。在此次大赛中我们团队基于声网&环信 SDK 构建了一个元宇宙应用 MetaTown,获得了环信专项奖,明年努力拿他个一等奖吧。
下面我会把这个参赛作品的实现分享给大家,欢迎一起交流~
关于MetaTown
金钱是被铸造出来的自由——陀思妥耶夫斯基
在三次元的现实世界,你是否为了搞钱而终日奔波?忍受996甚至007的非人待遇?是否正经历着创业人的凛冬?疫情等因素带来的本轮经济下行落实在每个人身上都是真真切切的,对于经历了40年经济暴增的国人来讲更是史无前例的。
在荷包日瘪萎靡不振的日子里,精神慰藉尤为重要,元宇宙正是当下最时髦的,何不创造一个可以躺着赚钱的元宇宙小镇?不为别的,在有虚拟工作的前提下每天我的虚拟角色的金币都会涨,想一想岂不是有一点小欢愉?还能在这个小镇结交一些志同道合沉迷于搞钱的友友们!
自然这些虚拟财富目前还是无法转化为真正的成就感的(变为现实财富),但现在市面上开始有人吹web3.0了!坐等币圈大佬入局,我们有信心打造一款能让一部分人先富起来的元宇宙小镇!
————————正经的分割线————————
MetaTown 是基于声网 RTC 和环信 IM 打造的模拟城市生活的社交类 App。
初来乍到的玩家首次进入 App 先选择不同的职业,在这座城市首先要考虑的是如何赚钱,或当程序员上下班打卡,或自己创业或去银行投资。还要注意身体健康,可能随机生病去医院,挂号咨询不同科室的医生。
这个小镇上的所有场所,均可以随时发起与陌生人私聊,共同兴趣结缘,独自体验在城市闯荡的日子!
MetaTown 核心技术
MetaTown 使用当下最流行的声网实时音视频以及环信即时通讯 SDK,具体场景如:
- 医院场景中一对一咨询医生,进行远程实时问诊。
- 社交场景中与好友实时音视频沟通,聊天。
1、声网音视频
MetaTown 运用了声网的实时音视频功能。
1)集成声网 SDK
1-1)添加声网音视频依赖在 app module 的 build.gradle 文件的 dependencies 代码块中添加如下代码:
implementation 'io.agora.rtc:full-rtc-basic:3.6.2'
然后在app module的build.gradle文件的android->defaultConfig代码块中添加如下代码:
ndk {
abiFilters "arm64-v8a"
}
// 设置支持的SO库架构(开发者可以根据需要,选择一个或多个平台的so)
1-2)添加必要权限 为了保证 SDK 能正常运行,我们需要在 AndroidManisfest.xml 文件中声明以下权限:
<!--允许程序连接网络-->
<uses-permission android:name="android.permission.INTERNET" />
<!--允许程序录制音频-->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!--允许程序使用照相设备-->
<uses-permission android:name="android.permission.CAMERA" />
<!--允许程序修改全局音频设置-->
<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" />
1-3)APP 在签名打包时防止出现混淆的问题需要在 proguard-rules.pro 文件里添加以下代码:
-keep class io.agora.**{*;}
2)创建并初始化 RtcEngine
)创建并初始化 RtcEngine
private void initializeEngine() {
try {
EaseCallKitConfig config = EaseCallKit.getInstance().getCallKitConfig();
if(config != null){
agoraAppId = config.getAgoraAppId();
}
mRtcEngine = RtcEngine.create(getBaseContext(), agoraAppId, mRtcEventHandler);
//因为有小程序 设置为直播模式 角色设置为主播
mRtcEngine.setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING);
mRtcEngine.setClientRole(CLIENT_ROLE_BROADCASTER);
EaseCallFloatWindow.getInstance().setRtcEngine(getApplicationContext(), mRtcEngine);
//设置小窗口悬浮类型
EaseCallFloatWindow.getInstance().setCallType(EaseCallType.CONFERENCE_CALL);
} catch (Exception e) {
EMLog.e(TAG, Log.getStackTraceString(e));
throw new RuntimeException("NEED TO check rtc sdk init fatal error\n" + Log.getStackTraceString(e));
}
}
3)设置视频模式
private void setupVideoConfig() {
mRtcEngine.enableVideo();
mRtcEngine.muteLocalVideoStream(true);
mRtcEngine.setVideoEncoderConfiguration(new VideoEncoderConfiguration(
VideoEncoderConfiguration.VD_1280x720,
VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15,
VideoEncoderConfiguration.STANDARD_BITRATE,
VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_PORTRAIT));
//启动谁在说话检测
int res = mRtcEngine.enableAudioVolumeIndication(500,3,false);
}
4)设置本地视频显示属性4-1)setupLocalVideo( VideoCanvas local ) 方法用于设置本地视频显示信息。应用程序通过调用此接口绑定本地视频流的显示视窗(view),并设置视频显示模式。 在应用程序开发中,通常在初始化后调用该方法进行本地视频设置,然后再加入频道。
private void setupLocalVideo() {
if(isFloatWindowShowing()) {
return;
}
localMemberView = createCallMemberView();
UserInfo info = new UserInfo();
info.userAccount = EMClient.getInstance().getCurrentUser();
info.uid = 0;
localMemberView.setUserInfo(info);
localMemberView.setVideoOff(true);
localMemberView.setCameraDirectionFront(isCameraFront);
callConferenceViewGroup.addView(localMemberView);
setUserJoinChannelInfo(EMClient.getInstance().getCurrentUser(),0);
mUidsList.put(0, localMemberView);
mRtcEngine.setupLocalVideo(new VideoCanvas(localMemberView.getSurfaceView(), VideoCanvas.RENDER_MODE_HIDDEN, 0));
}
4-2)joinChannel(String token,String channelName,String optionalInfo,int optionalUid ) 方法让用户加入通话频道,在同一个频道内的用户可以互相通话,多个用户加入同一个频道,可以群聊。 使用不同 App ID 的应用程序是不能互通的。如果已在通话中,用户必须调用 leaveChannel() 退出当前通话,才能进入下一个频道。
private void joinChannel() {
EaseCallKitConfig callKitConfig = EaseCallKit.getInstance().getCallKitConfig();
if(listener != null && callKitConfig != null && callKitConfig.isEnableRTCToken()){
listener.onGenerateToken(EMClient.getInstance().getCurrentUser(),channelName, EMClient.getInstance().getOptions().getAppKey(), new EaseCallKitTokenCallback(){
@Override
public void onSetToken(String token,int uId) {
EMLog.d(TAG,"onSetToken token:" + token + " uid: " +uId);
//获取到Token uid加入频道
mRtcEngine.joinChannel(token, channelName,null,uId);
//自己信息加入uIdMap
uIdMap.put(uId,new EaseUserAccount(uId,EMClient.getInstance().getCurrentUser()));
}
@Override
public void onGetTokenError(int error, String errorMsg) {
EMLog.e(TAG,"onGenerateToken error :" + error + " errorMsg:" + errorMsg);
//获取Token失败,退出呼叫
exitChannel();
}
});
}
}
完成以上配置后就可以发起呼叫了,其它一些摄像头控制,声音控制可以参考声网官网的API,这里不再赘述。
2、环信即时通讯
MetaTown 在6大交互场景中运用了环信的即时通讯 IM(Instant Messaging),给 IM 赋予了新的场景活力,支持陌生人私聊,群聊及超大型聊天室。
2-1)会话列表 项目中 IM 会话列表如下图:
- 会话列表关键代码:
public void show(BaseActivity activity) {
NiceDialog.init().setLayoutId(R.layout.dialog_message)
.setConvertListener(new ViewConvertListener() {
@Override
protected void convertView(ViewHolder holder, BaseNiceDialog dialog) {
RecyclerView rv = holder.getView(R.id.rv);
List<EaseConversationInfo> easeConversationInfos = initData();
rv.setLayoutManager(new LinearLayoutManager(dialog.getContext()));
DialogMsgAdapter dialogMsgAdapter = new DialogMsgAdapter(easeConversationInfos);
rv.setAdapter(dialogMsgAdapter);
dialogMsgAdapter.setOnItemClickListener(new DialogMsgAdapter.OnItemClickListener() {
@Override
public void onItemClick(int pos,String name) {
SoundUtil.getInstance().playBtnSound();
dialog.dismissAllowingStateLoss();
EMConversation item = (EMConversation) easeConversationInfos.get(pos).getInfo();
ChatDialog.getInstance().show(activity,item.conversationId(), name);
}
});
}
})
.setAnimStyle(R.style.EndAnimation)
.setOutCancel(true)
.setShowEnd(true)
.show(activity.getSupportFragmentManager());
}
2-2)IM 聊天
项目中 IM 聊天如下图:
- 发送消息关键代码:
@Override
public void sendMessage(EMMessage message) {
if(message == null) {
if(isActive()) {
runOnUI(() -> mView.sendMessageFail("message is null!"));
}
return;
}
addMessageAttributes(message);
if (chatType == EaseConstant.CHATTYPE_GROUP){
message.setChatType(EMMessage.ChatType.GroupChat);
}else if(chatType == EaseConstant.CHATTYPE_CHATROOM){
message.setChatType(EMMessage.ChatType.ChatRoom);
}
...
EMClient.getInstance().chatManager().sendMessage(message);
if(isActive()) {
runOnUI(()-> mView.sendMessageFinish(message));
}
}
- 接受消息关键代码:
public void onMessageReceived(List<EMMessage> messages) {
super.onMessageReceived(messages);
LiveDataBus.get().with(Constants.RECEIVE_MSG, LiveEvent.class).postValue(new LiveEvent());
for (EMMessage message : messages) {
// in background, do not refresh UI, notify it in notification bar
if(!MetaTownApp.getInstance().getLifecycleCallbacks().isFront()){
getNotifier().notify(message);
}
//notify new message
getNotifier().vibrateAndPlayTone(message);
}
}
3、场景原画
1)人物行走可分为踏步、水平移动两种动作,分别通过踏步动画和控制人物及背景 scrollview 移动实现。
2)关键点在于人物向左走过半屏继续向左行走,或向右走过半屏继续向右走的情况,以向右走为例,如果人物未超过屏幕中线,则控制人物向右移动;如果超出屏幕中线继续向右移动,则将人物固定在中线位置,背景向左滑动;如果背景向左已滑动至尽头,则保持背景不动,人物继续向右移动;如果人物移动至右边缘,则只控制人物原地踏步,背景和人物均不水平移动。
- 动画关键代码:
if (isToRight) {
ivPerson.setRotationY(180f);
}
isToRight = false;
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) ivPerson.getLayoutParams();
if (sv != null) {
if (layoutParams.leftMargin > DisplayUtil.getHeight(MetaTownApp.getApplication()) || !sv.canScrollHorizontally(-1)) {
layoutParams.leftMargin -= STEP;
layoutParams.leftMargin = Math.max(layoutParams.leftMargin, 50);
ivPerson.setLayoutParams(layoutParams);
} else {
sv.smoothScrollBy(-STEP, 0);
}
} else {
layoutParams.leftMargin -= STEP;
layoutParams.leftMargin = Math.max(layoutParams.leftMargin, 50);
ivPerson.setLayoutParams(layoutParams);
}
mAnimationDrawable.start();
if (!isToRight) {
ivPerson.setRotationY(0f);
}
isToRight = true;
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) ivPerson.getLayoutParams();
if (sv != null) {
if (layoutParams.leftMargin < DisplayUtil.getHeight(MetaTownApp.getApplication()) || !sv.canScrollHorizontally(1)) {
layoutParams.leftMargin += STEP;
layoutParams.leftMargin = Math.min(layoutParams.leftMargin, DisplayUtil.getWidth(MetaTownApp.getApplication()) - 100);
ivPerson.setLayoutParams(layoutParams);
} else {
sv.smoothScrollBy(STEP, 0);
}
} else {
layoutParams.leftMargin += STEP;
layoutParams.leftMargin = Math.min(layoutParams.leftMargin, DisplayUtil.getWidth(MetaTownApp.getApplication()) - 100);
ivPerson.setLayoutParams(layoutParams);
}
mAnimationDrawable.start();
项目开发过程中遇到了一些小问题,不过都通过查阅API以及相关资料顺利解决。
再说说后续计划
1)把现有的几个场景补齐(II期)
2)开发新场景,丰富搞钱路数
3)等一位币圈大佬掉到碗里。
以上是 MetaTown 作品在 RTE2022 编程挑战赛期间的实践分享,更多信息和作品可以访问官方渠道。
- 大赛官网:https://www.agora.io/cn/rte-hackathon-2022
- 大赛作品仓库:https://github.com/AgoraIO-Community/RTE-2022-Innovation-Challenge
(完)