/**
* 初始化生成组件
*/
private fun initBuilder() {
FUBuilderManager.setAIFaceProcessBundle(faceProcessBundlePath)//设置人脸驱动bundle
}
/**
* 初始化云平台
*/
private fun initCloud() {
FUCloudManager.setCloudConfig(FUDevHelper.getInstance().getDefaultCloudConfig())//设置云服务环境配置
FUCloudManager.setSdkVersion(FUDevHelper.getInstance().sdkVersion())//设置SDK版本
FUCloudManager.setProductVersion(FUDevHelper.getInstance().appVersion())//设置app版本
FUCloudManager.switchDebug(true)//设置是否开启日志调试
GlobalScope.launch {
FUCloudKit.getInstance().initTokenAndItemSource().collect {//获取token和云端资源配置表,改逻辑可放在欢迎页或者loading页
FULogger.i(TAG, "initTokenAndItemSource :$it")
}
}
}/**
* 图片人脸检测
* @param bitmap Bitmap
* @return AvatarBuilderBean
*/
fun trackFace(bitmap: Bitmap): FaceTrackResultBean {
mTrackReentrantLock.withLock {
val single = FaceTracSingleFaceFilter()
return mFUBuilderKit.trackFace(
bitmap = bitmap,
detectMode = FUAIFaceDetectModeEnum.IMAGE,
filters = arrayOf(single)
)
}
}
/**
* 人脸检测每一帧数据
* @param inputData FURenderInputData
*/
fun trackFaceCurrentFrame(inputData: FURenderInputData): FaceTrackResultBean {
val imageConfig = getImageConfig(inputData, inputData.renderConfig.isCameraFacingFront)
?: return FaceTrackResultBean(inputData, status = FUBuildStatusEnum.NO_FACE)
val rotMode = getFUAIRotationRotMode(imageConfig)
val image = FUAIImageData(
imageConfig.imageFormat,
imageConfig.imageWidth,
imageConfig.imageHeight,
imageConfig.imageBuffer,
imageConfig.imageWidth
)
val expressionFilter = FaceTracExpressionFilter(level = 0.6f)
val singleFilter = FaceTracSingleFaceFilter()
val rotationFilter = FaceTrackRotationFilter(rotate = 90)
val rectFilter = FaceTracRectFilter()
return FUBuilderKit.getInstance().trackFace(
imageData = image,
detectMode = FUAIFaceDetectModeEnum.VIDEO,
rotMode = rotMode,
filters = arrayOf(singleFilter, rectFilter, expressionFilter, rotationFilter)
)
}/**
* 通过bitmap调用云端接口生成avatar形象的json配置
*/
class CreateAvatarByCloudUseCase(val ioDispatcher: CoroutineDispatcher = Dispatchers.Default) {
companion object{
const val TAG = "CreateAvatarByCloudUseCase"
}
suspend fun execute(params: Params): Result = withContext(ioDispatcher) {
val time = System.currentTimeMillis()
return@withContext suspendCancellableCoroutine {con->
FUBuilderKit.getInstance().buildAvatar(
params.bitmap,
isMale = params.isMale,
listener = object : OnBuildAvatarListener {
override fun onSuccess(data: String) {
val endTime = System.currentTimeMillis()
FULogger.i(TAG) { "build used time " + (endTime - time) }
con.resume(Result.success(data))
}
override fun onError(code: String, message: String) {
runBlocking {
FUCloudKit.getInstance().updateToken().collect{
when (it) {
is NetResponseState.Exception -> {
FULogger.e(FuResourceFactoryImpl.TAG, "updateToken Exception ${it.throwable}")
con.resume(Result.failure(Throwable("创建形象失败:code=404 message=updateToken Exception ${it.throwable}")))
}
is NetResponseState.Success -> {
FULogger.i(FuResourceFactoryImpl.TAG, "updateToken Success ${it.data?.token}")
FUBuilderKit.getInstance().buildAvatar(params.bitmap, isMale = params.isMale, listener = object :OnBuildAvatarListener{
override fun onError(code: String, message: String) {
FULogger.e(TAG) { "buildAvatar again error $message" }
con.resume(Result.failure(Throwable("创建形象失败:code=$code message=$message")))
}
override fun onSuccess(data: String) {
val endTime = System.currentTimeMillis()
FULogger.i(TAG) { "buildAvatar again used time " + (endTime - time) }
con.resume(Result.success(data))
}
})
}
is NetResponseState.Loading -> {
FULogger.i(FuResourceFactoryImpl.TAG, "updateToken Loading")
}
is NetResponseState.Error -> {
FULogger.e(FuResourceFactoryImpl.TAG, "updateToken Error ${it.code} ${it.message}")
con.resume(Result.failure(Throwable("创建形象失败:code=${it.code} message=${it.message}")))
}
}
}
}
}
})
}
}
data class Params(
val bitmap: Bitmap,
val version: String = "2022-03-01",
val isMale: Boolean
)
}快速开始->快速跑通->必要信息配置来获取自己的证书、云平台配置信息、Mos云服务地址。1、证书文件:authpack.java 2、云平台配置信息: baseUrl = " https://avatarxapi.faceunity.com ", apiKey = "41rCEUV1vBvP63hryPo6JW", apiSecret = "xrbBu3gy55253xbEJqSkS5", appId = "WJ6oPyrh36PvBv1VUECr14" 3、Mos云服务地址:ws://47.98.208.52:8000
/Gradle Scripts/build.gradle(Project:) 文件中添加 Maven仓库支持buildscript {
repositories {
...
maven {
url 'http://maven.faceunity.com/repository/maven-public/'
allowInsecureProtocol true
}
maven { url 'https://jitpack.io' }
}
}
allprojects {
repositories {
...
maven {
url 'http://maven.faceunity.com/repository/maven-public/'
allowInsecureProtocol true
}
maven { url 'https://jitpack.io' }
}
}/Gradle Scripts/build.gradle(Module:.app) 中添加如下依赖:...
dependencies {
... // 1.0.0 为具体版本号 // 最新版本号请咨询
implementation 'com.faceunity.mos:full:1.0.0'
}如使用MOS中的渲染引擎的话,需要导入基础资源并配置,请参考AvatarX SDK快速开始 快速开始->SDK 集成->导入资源,基础配置文件如下:
/**
* 描述:通用资源配置实现
*
* @author hexiang on 2023/2/15
*/
class FuCommonResourceConfigImpl : IFuMosResourceConfig { /** * 图形引擎初始化必要资源 * engine/EngineAssets.bundle */
override fun sdkEngineAssetsBundle(): String = "engine/EngineAssets.bundle" /** * model/ai_face_processor_e47_s1.bundle */
override fun aiFaceProcessorBundle(): String = "model/ai_face_processor_e47_s1.bundle" /** * 形象默认配置文件 * GAssets/config/controller_config.bundle */
override fun avatarConfigBundlePath(): String = "GAssets/config/controller_config.bundle" /** * 默认动画Graph配置 */
override fun animGraphPath(): String = "graph/animGraph.json" /** * 默认动画Logic配置 */
override fun animLogicPath(): String = "graph/animLogic.json" /** * 出生动画的配置 */
override fun birthAnimationConfig(): String ="chat/main/single_interact_animation_config.json" /** * Avatar 形象缓存 目录 * 存放 avatar.json和avatar.png */
override fun avatarLocalDir(): String = FUFileUtils.getExternalRootFileDir().absolutePath + File.separator + "mos" + File.separator + "avatar" /** * avatar的bundle下载缓存根目录 */
override fun avatarBundleLocalRootDir(): String = FUFileUtils.getExternalRootFileDir().absolutePath + File.separator
}assets/chat/animation:为mos功能中所需要的动画资源
assets/chat/main:普通语聊房间的场景配置资源
assets/chat/cinema:私人影院房间的场景配置资源
assets/chat/werewolf:狼人杀房间的场景配置资源

基本权限配置
/app/src/main/AndroidManifest.xml 文件中添加如下代码,获取相应的设备权限

动态权限申请
com.faceunity.app_mos.ui.home.FuMosHomeFragment#checkSelfPermission/** * mos需要的权限 * 如果targetSdkVersion>=33,需要特殊处理文件读写权限 */private val mPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
arrayListOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.READ_MEDIA_IMAGES
)
} else {
arrayListOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
}... ...
快速开始-环境初始化-SDK鉴权初始化快速开始-环境初始化-初始化资源管理组件快速开始-环境初始化-初始化云服务组件@param commonResourceConfig 通用资源配置 (参考多人互动 )
@param simpleRoomResourceConfig 普通语聊房场景配置,不需要可为null (参考多人互动 )
@param cinemaRoomResourceConfig 影院场景配置,不需要可为null
@param werewolfRoomResourceConfig 狼人杀场景配置,不需要可为null
/** * 初始化 * @param resourceCloudConfig avatar资源云端配置 * @param mosCloudUrl mos的云服务地址 * @param commonResourceConfig 通用资源配置 * @param simpleRoomResourceConfig 普通语聊房场景配置,不需要可为null * @param cinemaRoomResourceConfig 影院场景配置,不需要可为null * @param werewolfRoomResourceConfig 狼人杀场景配置,不需要可为null */FuMosKit.getInstance().init( resourceCloudConfig = testV3CloudConfig, mosCloudUrl = testV3MosCloudUrl, commonResourceConfig = FuCommonResourceConfigImpl(), simpleRoomResourceConfig = FuSimpleChatRoomResourceConfig(), cinemaRoomResourceConfig = FuCinemaResourceConfig(), werewolfRoomResourceConfig = FuWerewolfResourceConfig() )
/** * mos日志等级配置,不配置-》默认OFF不打印日志 * VERBOSE(0), DEBUG(1), INFO(2), WARN(3), ERROR(4), OFF(5); */FuMosKit.getInstance().initMosLog(FuMosLogger.LogLevel.DEBUG)
/** * 登录 * @param nickname 昵称,可后期调用updateBaseUserInfo来更新 * @param callbackListener 返回userId * @param friendInvitationListener 好友邀请的监听(仅接收login后,到createRoom、joinRoom之前的邀请通知) */fun login( nickname: String, avatarId: String, avatarPngUrl: String, callbackListener: FuMosResultListener, friendInvitationListener: NormalCallback? = null )/** * 是否登录 */fun isLogin(): Boolean/** * 返回本地用户userId */fun getUserId(): Int/** * 是否事本地用户 */fun isLocalUser(userId: Int): Boolean
/** * 更新用户信息 * @param nickname 昵称 * @param avatarId Avatar形象id * @param avatarPngUrl 头像 */fun updateBaseUserInfo(nickname: String, avatarId: String, avatarPngUrl: String, listener: FuMosResultListener?)
com.faceunity.app_mos.ui.home.FuMosHomeViewModelfun loginAndRequest() {
if (isLogin()) {
...
return
}
//设置的昵称,不存在时获取随机昵称
var nickname = getNickname()
if (TextUtils.isEmpty(nickname)) {
nickname = StringUtils.getRandomEnglishStr(6)
mNickName = nickname
FuSPStorageFieldUtils.set(SP_NICKNAME, nickname)
}
FuMosKit.getInstance().login(nickname, mAvatarId, mAvatarPngUrl,
object : FuMosResultListener {
override fun onSuccess(userId: Int) {
queryRooms()
}
override fun onError(code: Int, error: String) {
ToastUtils.showNormalToast(error)
mRefreshCompleted.postValue(null)
}
},
object : NormalCallback {
override fun callback(data: FuRoomFriendInvitationInfo) {
//对多个邀请通知进行入队
listInvitation.offer(data)
handleInvitation()
}
})
}FuMosKit.getInstance().leave()
/** * 创建房间 * @param roomType 房间类型 * @param rangeMax 私聊范围识别圈的大小,默认200 * @param birthPositions 出生位置 * @param callbackListener 返回房间信息,根据返回的房间信息加入房间 */fun createRoom( roomType: Common.RoomType, rangeMax: Float = 200f, birthPositions: List, callbackListener: FuMosResultListener)
参数1-roomType:房间类型(根据所需房间创建)
public enum RoomType{
/**
* 普通语聊房间
*/
Chat(0),
/**
* 影院
*/
Cinema(1),
/**
* 狼人杀
*/
Werewolf(2)
}参数2-rangeMax: 私聊范围触发的识别圈大小,默认200
参数3-birthPositions :出生位置
/** * 普通语聊房的出生位置 */ private val mSimpleBirths = arrayListOf( FUCoordinate3DData(x = -302f, y = 0f, z = -218f), FUCoordinate3DData(x = 123f, y = 0f, z = -462f), FUCoordinate3DData(x = 646f, y = 0f, z = -286f), FUCoordinate3DData(x = -138f, y = 0f, z = 236f), FUCoordinate3DData(x = 344f, y = 0f, z = 233f), FUCoordinate3DData(x = -536f, y = 0f, z = 388f), FUCoordinate3DData(x = -105f, y = 0f, z = 743f), FUCoordinate3DData(x = 481f, y = 0f, z = 699f) ) /** * 影院的出生位置 */ private val mCinemaBirths = arrayListOf( FUCoordinate3DData(x = -628f, y = 0f, z = 35f), FUCoordinate3DData(x = -285f, y = 0f, z = 101f), FUCoordinate3DData(x = -70f, y = 0f, z = -289f), FUCoordinate3DData(x = 226f, y = 0f, z = 136f), FUCoordinate3DData(x = 65f, y = 0f, z = 326f), FUCoordinate3DData(x = 441f, y = 0f, z = -215f), FUCoordinate3DData(x = -462f, y = 0f, z = 699f), FUCoordinate3DData(x = 406f, y = 0f, z = 752f) ) /** * 狼人杀的出生位置 */ private val mWerewolfBirths = arrayListOf( FUCoordinate3DData(x = -3.09f, y = 0f, z = -244.934f), FUCoordinate3DData(x = 211.281f, y = 0f, z = -121.875f), FUCoordinate3DData(x = 206.422f, y = 0f, z = 122.413f), FUCoordinate3DData(x = -2.104f, y = 0f, z = 240.732f), FUCoordinate3DData(x = -206.834f, y = 0f, z = 123.124f), FUCoordinate3DData(x = -211.186f, y = 0f, z = -122.585f) )
com.faceunity.app_mos.ui.home.FuMosHomeViewModel基础功能->加入房间->获取房间列表后选择房间加入->根据房间信息加入房间fun create(roomType: RoomType,gender: FuAvatarGender) {
val birthPositions = when (roomType) {
RoomType.Chat -> mSimpleBirths
RoomType.Cinema -> mCinemaBirths
RoomType.Werewolf -> mWerewolfBirths
else -> mSimpleBirths
}
FuMosKit.getInstance().createRoom(roomType, rangeMax = 200f, birthPositions, object : FuMosResultListener {
override fun onSuccess(roomInfo: Common.RoomInfo) {
join(roomInfo.url, roomInfo.type, roomInfo.roomId,gender)
}
override fun onError(code: Int, error: String) {
ToastUtils.showNormalToast(error)
}
})
}com.faceunity.app_mos.ui.home.FuMosHomeFragment#checkSelfPermission/** * 加入房间 * @param hostUrl 连接地址,可空 * @param roomType 房间类型,可空 * @param roomId 房间id * @param gender 形象性别 * @param callbackListener 返回本地用户信息 */fun joinRoom( hostUrl: String? = null, roomType: Common.RoomType?, roomId: String, gender: FuAvatarGender, callbackListener: FuMosResultListener)
FuMosKit.getInstance()
.joinRoom(null, null, roomId, gender, object : FuMosResultListener {
override fun onSuccess(data: FuJoinRoomResponse) { //加入成功后返回房间信息,根据不同的roomType打开不同的房间模板
}
override fun onError(code: Int, error: String) { //加入房间失败,error返回错误信息
ToastUtils.showNormalToast(error)
}
})/** * 查询名下所有房间 * @param callbackListener 返回房间列表 */fun queryRooms(callbackListener: FuMosResultListener)
FuMosKit.getInstance().queryRooms(object : FuMosResultListener {
override fun onSuccess(data: List) {
mRefreshCompleted.postValue(null)
if (data.isEmpty()) {
mRoomEmpty.postValue(null)
return
}
mMapRooms.clear()
for (item in mListRoomTypes) {
mMapRooms[item.type] = ArrayList()
}
for (roomInfo in data) {
mMapRooms[roomInfo.type]?.let {
it.add(roomInfo)
}
}
mRoomListData.postValue(mMapRooms)
}
override fun onError(code: Int, error: String) {
ToastUtils.showNormalToast(error)
mRefreshCompleted.postValue(null)
}
})join(roomInfo.url, roomInfo.type, roomInfo.roomId, mViewModel.getGender())
fun join(hostUrl: String?, roomType: RoomType?, roomId: String,gender: FuAvatarGender) {
FuMosKit.getInstance()
.joinRoom(hostUrl, roomType, roomId, gender, object : FuMosResultListener {
override fun onSuccess(data: FuJoinRoomResponse) {
//加入成功后返回房间信息,根据不同的roomType打开不同的房间模板
_joinRoomLiveData.postValue(data)
}
override fun onError(code: Int, error: String) {
ToastUtils.showNormalToast(error)
}
})
}data class FuJoinRoomResponse( val roomId: String, val roomType: Common.RoomType, val userInfo: Common.UserInfo?, val success: Boolean, val msg: String? = null )
好友邀请的通知
基础功能->登录->登录会注入好友邀请的监听@param friendInvitationListener 好友邀请的监听(仅接收login后,到createRoom、joinRoom之前的邀请通知)/** * 好友邀请信息 * @param inviteInfo 邀请信息 * @param roomInfo 邀请的房间信息 */data class FuRoomFriendInvitationInfo(val inviteInfo: FriendInvitationNotification, val roomInfo: RoomInfo)
对好友邀请通知的处理
com.faceunity.app_mos.ui.home.FuMosHomeViewModel#handleInvitation根据好友邀请信息中的roomInfo加入房间,参考多人互动 基础功能->加入房间->获取房间列表后选择房间加入->根据房间信息加入房间
在SDK初始化的时候设置simpleRoomResourceConfig,参考多人互动 - 相应的assets资源放置好
/** * 描述:普通语聊房资源配置 */class FuSimpleChatRoomResourceConfig : IFuMosSceneResourceConfig { /** * 场景:chat/werewolf/scene/langrensha.bundle */
override fun getScenePath(): String = "chat/main/scene/Scene_B_jiantou.bundle" /** * 机位:chat/werewolf/scene/cam_langrensha_cj_rw.bundle */
override fun getCameraPath(): String = "chat/main/scene/Cinemachine_scene_A_small.bundle" /** * 私聊场景的机位 */
override fun getPrivateCameraPath(): String = "chat/main/scene/cam_AXdemo_icon_texie.bundle" /** * 私聊场景的灯光 */
override fun getPrivateSceneLightPath(): String = "chat/main/scene/light_man.bundle" /** * config/MobileRenderConfig.json */
override fun getMobileRenderConfigPath(): String = "config/MobileRenderConfig.json" /** * 寻路脚本配置 */
override fun getNavigationScriptPath(): String = "chat/main/script/Scene_B_script.bundle" /** * 动画Logic配置 */
override fun getLogicConfigPath(): String = "chat/main/script/graph/animLogic.json" /** * 动画Graph配置 */
override fun getGraphConfigPath(): String = "chat/main/script/graph/AnimGraph.json" /** * 脚本动画配置 */
override fun getScriptAnimConfigPath(): String = "chat/main/script/json/scriptAnimations.json"
override fun getDecorateConfigPath(): String = ""
override fun getDecorateRootPath(): String = "" /** * 单人动画配置表 * */
override fun getSingleAnimConfigPath(): String = "chat/main/single_interact_animation_config.json"
override fun getDoubleAnimConfigPath(): String = "" /** * 动画资源根目录:chat/animation */
override fun getAnimationRootPath(): String = "chat/animation"
}app-mos/src/main/java/com/faceunity/app_mos/ui/simple/FuSimpleChatRoomActivity.ktcom.faceunity.lib_mos.modules.ui.FuSimpleChatRoomFragmentlayout代码:(可参考demo中app-mos/src/main/res/layout/activity_fu_simple_chat_room.xml)
activity代码:
app-mos/src/main/java/com/faceunity/app_mos/ui/simple/FuSimpleChatRoomActivity.kt...
import com.faceunity.lib_mos.help.FUChatConstant
import com.faceunity.lib_mos.interfaces.FuMosResultListener
import com.faceunity.lib_mos.interfaces.IFuChatListener
import com.faceunity.lib_mos.interfaces.NormalCallback
import com.faceunity.lib_mos.model.FuChatRoomTextMessage
import com.faceunity.lib_mos.modules.rtcchat.FuChatChannelType
import com.faceunity.lib_mos.modules.rtcchat.video.FuSimpleVideoChatView
import com.faceunity.lib_mos.modules.rtcchat.video.IFuVideoChatCanvas
import com.faceunity.lib_mos.modules.ui.FuSimpleChatRoomFragment
.../** * 1.创建FuSimpleChatRoomFragment */private val mChatRoomFragment: FuSimpleChatRoomFragment by lazy { FuSimpleChatRoomFragment() }
...
override fun onCreate(savedInstanceState: Bundle?) {
intent?.apply {
/**
* mRoomId房间号:加入房间的回调中会返回roomId
*/
mRoomId = getStringExtra(FUChatConstant.IntentKey.ROOMID).toString()
/**
* mUserInfo本地用户信息:加入房间的回调中会返回userInfo
*/
mUserInfo = getSerializableExtra(FUChatConstant.IntentKey.USERINFO) as UserInfo /** * 2.注入数据 */
mChatRoomFragment.injectData(mRoomId,mUserInfo) /** * 3.注入监听 */
mChatRoomFragment.injectListener(mChatListener)
/**
* FPS 标记:可实现IFPSChecker,来打印或查看场景的fps,分析性能
*/
mChatRoomFragment.injectFPSChecker(MosFPSChecker()) /** * 4.绑定数据:用于界面恢复的时候 */
mChatRoomFragment.arguments = Bundle().apply{
putString(FUChatConstant.IntentKey.ROOMID,mRoomId)
putSerializable(FUChatConstant.IntentKey.USERINFO,mUserInfo)
} /** * 5.添加fragment到activity中 */
supportFragmentManager.beginTransaction().replace(R.id.fg_content, mChatRoomFragment).commitAllowingStateLoss()
...
}/** * 本地公聊频道麦克风是否可以自由控制 * 可以控制麦克风按钮是否可以点击 * @param enabled false:开启全员闭麦,不可随意控制麦克风 * @param channelType 频道类型 */fun onLocalMicrophoneEnabled(enabled: Boolean, channelType: FuChatChannelType) {}/** * 连接断开 */fun onDisconnect()/** * 需要在这里做页面的销毁 * 用户主动退出房间、被踢出房间、房间解散都会收到改通知 */fun onLeave()private val mChatListener = object : IFuChatListener() { /** * 本地公聊频道麦克风是否可以自由控制 * 可以控制麦克风按钮是否可以点击 * @param enabled false:开启全员闭麦,不可随意控制麦克风 * @param channelType 频道类型 */
override fun onLocalMicrophoneEnabled(enabled: Boolean, channelType: FuChatChannelType) {
//("本地公聊频道麦克风是否可以自由控制")
if (enabled) {
mBinding.tvPublicMicrophone.setImageResource(R.drawable.icon_fu_chat_mic_off)
} else {
mBinding.tvPublicMicrophone.setImageResource(R.drawable.icon_cinema_room_mic_off)
}
}
override fun onDisconnect() {
FuRoomDisconnectHintDialog(this@FuSimpleChatRoomActivity)
.injectListener(object : NormalCallback {
override fun callback(data: Any?) {
mChatRoomFragment.leaveRoom()
}
})
.show()
}
override fun onLeave() {
finish()
}
...
}房主可调用mChatRoomFragment.closeAllMicForPublic(isCloseAll)方法来开启/关闭公聊房间的全员闭麦功能;
其他房间内的成员会收到IFuChatListener中的onLocalMicrophoneEnabled方法的回调,来对按钮的显示做处理,参考多人互动 实现IFuChatListener基本方法-onLocalMicrophoneEnabled
/** * 开启全员闭麦 * @param isCloseAll true:开启全员闭麦;false:关闭全员闭麦 */fun closeAllMicForPublic(isCloseAll: Boolean)
在页面中可根据自己的需要来控制相应按钮的显示,参考demo:com.faceunity.app_mos.ui.simple.FuSimpleChatRoomActivity#setPublicMicStateByMaster
/** * 群主设置麦克风状态 * 只有群主可以控制 */private fun setPublicMicStateByMaster(isCloseAll: Boolean) {
mChatRoomFragment.closeAllMicForPublic(isCloseAll)
if (!isCloseAll) {
mBinding.tvMicrophoneAll.setImageResource(R.drawable.icon_fu_chat_all_mic_turnoff)
ToastUtils.showNormalToast("已关闭全员闭麦")
} else {
//当前是全员闭麦
mBinding.tvMicrophoneAll.setImageResource(R.drawable.icon_fu_chat_all_mic_turnon)
ToastUtils.showNormalToast("已开启全员闭麦")
}
}调用方法mChatRoomFragment.publicMic(openMicro),返回值true、false表示是否成功的控制麦克风
/** * 控制公聊麦克风 * @param open true:开启麦克风 * @return 是否成功 */fun publicMic(open: Boolean)/** * 获取公聊麦克风状态 */fun getPublicMicState(): Boolean
调用方法mChatRoomFragment.publicAudio(open)
/** * 控制公聊听筒 * @param open true:开启听筒 * @return 是否成功 */fun publicAudio(open: Boolean): Boolean /** * 公聊听筒状态 */fun getPublicAudioState(): Boolean
调用方法mChatRoomFragment.privateMic(open)
/** * 控制私聊麦克风 * @param open true:开启麦克风 */fun privateMic(open: Boolean): Boolean/** * 私聊麦克风状态 */fun getPrivateMicState(): Boolean
调用方法mChatRoomFragment.privateAudio(open)
/** * 控制私聊听筒 * @param open true:开启听筒 */fun privateAudio(open: Boolean): Boolean/** * 获取私聊听筒状态 */fun getPrivateAudioState(): Boolean
在房间内调用mChatRoomFragment.queryHomeUsers(searchContent, listener)
支持精准搜索nickname或者user_id
/** * 查询大厅内用户 * 目前限制50人 * @param searchContent 搜索内容(nickname或者user_id) */fun queryHomeUsers(searchContent: String?, listener: FuMosResultListener)
通过mChatRoomFragment.queryHomeUsers(searchContent, listener)可查询到可以邀请的用户(限制50人),然后调用mChatRoomFragment.sendFriendInvitation(userId)来邀请指定用户。参考demo中:com.faceunity.app_mos.ui.setting.RoomSettingDialog.SettingCallback#inviteFriends
邀请好友的UI可参考demo中:com.faceunity.app_mos.ui.invite.CinemaInviteFriendsDialog
/** * 邀请好友 * @param userId 需要邀请的好友的用户id */fun sendFriendInvitation(userId: Int)
被邀请的好友会收到邀请通知,参考多人互动 基础功能-通过邀请加入房间
调用mChatRoomFragment.queryRoomUsersExcludeLocal可查询当前房间内所有用户列表(不包括本地用户)
/** * 查询当前房间中所有用户-包括本地用户 */fun queryRoomUsers(listener: FuMosResultListener)/** * 查询当前房间中所有用户-不包括本地用户 */fun queryRoomUsersExcludeLocal(listener: FuMosResultListener)
调用mChatRoomFragment.queryRoomUsersExcludeLocal可查询当前房间内所有用户列表后,可得到想踢的人的用户信息,然后调用mChatRoomFragment.removeUserFromRoom(userId)将指定userId的用户踢出房间
/** * 踢人 * @param userId 要踢出的用户id */ fun removeUserFromRoom(userId: Int)
被踢出的用户会收到相应通知,执行退出房间的逻辑,收到onLeave回调,参考多人互动 实现IFuChatListener基本方法-onLeave
房主和非房主都是调用mChatRoomFragment.leaveRoom()来退出房间的,要早收到onLeave回调再销毁页面参考多人互动 实现IFuChatListener基本方法-onLeave
需要在初始化前配置好单人动作资源,参考多人互动 getSingleAnimConfigPath,见demo中:app-mos/src/main/assets/chat/main/single_interact_animation_config.json
普通语聊房间内,点击avatar本身会触发随机单人动作
获取配置好的所有单人动作:mChatRoomFragment.getAllSingleAnimations()
执行指定动作:mChatRoomFragment.playSingleAnimationAction(type)
/** * 描述:单人互动 * */data class FuAvatarInteractBean(
var type: String,//类型 BiXin...
var title: String,
var iconResName: String,//图片资源string名称
var distance: Float,
var anim: FuAvatarInteractGenderBean //动画
)/** * @param female 女生 * @param male 男生 */data class FuAvatarInteractGenderBean(val female: FUChatAnimBean, val male: FUChatAnimBean) {
fun getAnimBean(gender: FuAvatarGender): FUChatAnimBean {
return if (gender == FuAvatarGender.Female) female else male
}
}/** * 描述: * @param propPathName 体外道具的相对路径(文件名) * @param pathName 动画的相对路径(文件名) * @param repeatable 是否循环播放 * @param yaw 朝向,为null室表示不固定朝向 */data class FUChatAnimBean(val propPathName: String, val pathName: String, val repeatable: Boolean, val yaw: Float?=null)/** * 进入了私聊房间(两个avatar靠近会触发该回调) * 触发距离由房主在创建房间的时候设置rangeMax */open fun onJoinPrivate(){}/** * 离开了私聊房间(两个avatar原理会触发该回调) */open fun onLeavePrivate(){}/** * 监测到远端最活跃用户回调 * 如果最活跃用户一直是同一位用户,则 SDK 不会再次触发 onActiveSpeaker 回调。 * 如果最活跃用户有变化,则 SDK 会再次触发该回调并报告新的最活跃用户的 uid。 * @param videoChatCanvas 视频容器 * @param uid 用户id */open fun onActiveSpeaker(videoChatCanvas: IFuVideoChatCanvas?, uid: Int) {}/** * 远端用户麦克风的状态 * @param uid * @param open 是否开启麦克风 */open fun onRemoteMicrophoneState(videoChatCanvas: IFuVideoChatCanvas?, uid: Int, open: Boolean, channelType: FuChatChannelType) {}/** * 本地用户视频开启 * @param open true开启视频、false关闭视频 * @param uid 用户id * @param roomId 房间id * @param channelType 频道类型 */open fun onLocalVideoOpen(uid: Int, roomId: String, channelType: FuChatChannelType){}
open fun onLocalVideoClose(videoChatCanvas: IFuVideoChatCanvas?, uid: Int, roomId: String, channelType: FuChatChannelType){}/** * 远端用户视频开启 * @param open true开启视频、false关闭视频 * @param uid 用户id * @param roomId 房间id * @param channelType 频道类型 */open fun onRemoteVideoOpen(uid: Int, roomId: String, channelType: FuChatChannelType){}
open fun onRemoteVideoClose(videoChatCanvas: IFuVideoChatCanvas?, uid: Int, roomId: String, channelType: FuChatChannelType){}/** * 远端用户视频首帧渲染 */open fun onRemoteVideoFirstFrame(videoChatCanvas: IFuVideoChatCanvas, uid: Int, roomId: String, channelType: FuChatChannelType){}/** * 本地采集的视频帧 * 可根据需求对其进行修改(比如:人脸显示avatar形象) */open fun onCaptureVideoFrame(
data: ByteArray, frameType: Int, width: Int, height: Int, bufferLength: Int,
yStride: Int, uStride: Int, vStride: Int, rotation: Int, renderTimeMs: Long
) {
}onJoinPrivate、onLeavePrivate表示本地用户进入或离开私聊,可以用来显示/隐藏私聊麦克风、扬声器控制按钮
onLocalVideoOpen、onLocalVideoClose来绘制、移除本地用户的视频显示框;FuSimpleVideoChatView中可实现切换头像的事件setSwatchAvatarListener ,做到真实人脸和avatar切换显示
updateVideoState来控制是否将视频框在页面上Visiable,默认不显示。一般远端用户的视频框需要处理首帧渲染好后再显示
mChatRoomFragment.setLocalVideoRenderView(privateVideView, roomId, uid)来绑定本地视频渲染控件
app-mos/src/main/java/com/faceunity/app_mos/widget/video/FuSimpleVideoChatView.kt为承载视频的自定义容器,可根据需要自由实现;自定义视频容器承载器:mBinding.layoutVideo为app-mos/src/main/java/com/faceunity/app_mos/widget/video/FuPrivateChatVideoContainer.kt是对所有展示出来的视频容器的布局,也可自由实现
/** * 本地用户视频开启 * @param open true开启视频、false关闭视频 * @param uid 用户id * @param roomId 房间id * @param channelType 频道类型 */override fun onLocalVideoOpen(uid: Int, roomId: String, channelType: FuChatChannelType) {
// "本地用户视频开启"
val privateVideView = FuSimpleVideoChatView(this@FuSimpleChatRoomActivity)
// privateVideView.updateVideoState(true)
privateVideView.setSwatchAvatarListener {
//切换头像
mChatRoomFragment.setPrivateChatOpenAvatarFlag(!mChatRoomFragment.getPrivateChatOpenAvatarFlag())
}
mBinding.layoutVideo.addViewByUid(uid, privateVideView)
mChatRoomFragment.setLocalVideoRenderView(privateVideView, roomId, uid)
//延时显示,防止首帧黑屏
privateVideView.postDelayed({
privateVideView.updateVideoState(true)
},500)
}
override fun onLocalVideoClose(videoChatCanvas: IFuVideoChatCanvas?, uid: Int, roomId: String, channelType: FuChatChannelType) {
//("本地用户视频关闭")
videoChatCanvas?.let {
//如果是私聊状态下,本地用户退出,就将所有video都移除(因为leaveChannel后其他人的Offline不一定能收到)
if (channelType == FuChatChannelType.Private) {
mBinding.layoutVideo.removeAllViews()
} else {
mBinding.layoutVideo.removeViewByUid(uid, it.parentView())
}
}
}通过onCaptureVideoFrame来对本地用户采集的视频数据进行特殊处理,如显示人物AR化显示avatar头像;
/** * 本地采集的视频帧 * 可根据需求对其进行修改(比如:人脸显示avatar形象) */override fun onCaptureVideoFrame(
data: ByteArray, frameType: Int, width: Int, height: Int, bufferLength: Int,
yStride: Int, uStride: Int, vStride: Int, rotation: Int, renderTimeMs: Long
) {
//("本地采集的视频帧")
mChatRoomFragment.modifyVideoData(width, height, data, yStride, uStride, vStride)
}通过onRemoteVideoOpen、onRemoteVideoClose来绘制、移除远端用户的视频显示框,为了防止远端视频还未准备好,可以在远端首帧回调onRemoteVideoFirstFrame中再显示对应的视频框;
通过onRemoteMicrophoneState监测远端用户麦克风的状态
通过onActiveSpeaker监测到远端最活跃用户回调
/** * 远端用户视频开启 * @param open true开启视频、false关闭视频 * @param uid 用户id * @param roomId 房间id * @param channelType 频道类型 */override fun onRemoteVideoOpen(uid: Int, roomId: String, channelType: FuChatChannelType) {
//("远端用户视频开启")
//TODO 处理动画的问题
if (channelType == FuChatChannelType.Private) {
mChatRoomFragment.updatePrivateState(true, uid)
}
val privateVideView = FuSimpleVideoChatView(this@FuSimpleChatRoomActivity)
privateVideView.updateVideoState(false)
mBinding.layoutVideo.addViewByUid(uid, privateVideView)
mChatRoomFragment.setRemoteVideoRenderView(privateVideView, roomId, uid)
}
override fun onRemoteVideoClose(videoChatCanvas: IFuVideoChatCanvas?, uid: Int, roomId: String, channelType: FuChatChannelType) {
//("远端用户视频关闭")
if (channelType == FuChatChannelType.Private) {
mChatRoomFragment.updatePrivateState(false, uid)
}
videoChatCanvas?.let {
mBinding.layoutVideo.removeViewByUid(uid, it.parentView())
}
}/** * 远端用户视频首帧渲染 */override fun onRemoteVideoFirstFrame(videoChatCanvas: IFuVideoChatCanvas, uid: Int, roomId: String, channelType: FuChatChannelType) {
//首帧渲染的问题需要处理下
val videoChatView = videoChatCanvas.parentView() as FuSimpleVideoChatView
videoChatView.postDelayed({
videoChatView.updateVideoState(true)
},500)
}private val mChatListener = object : IFuChatListener() {
/**
* ----- 私聊相关 -----
*/
override fun onJoinPrivate() {
showPrivateBtn()
mChatRoomFragment.updatePrivateState(true, mUserInfo.userId)
}
override fun onLeavePrivate() {
hidePrivateBtn()
mChatRoomFragment.updatePrivateState(false, mUserInfo.userId)
}
override fun onActiveSpeaker(videoChatCanvas: IFuVideoChatCanvas?, uid: Int) {
//("监测到远端最活跃用户回调")
videoChatCanvas?.apply {
(parentView() as FuSimpleVideoChatView).updateSpeakerState(true)
}
}
override fun onRemoteMicrophoneState(videoChatCanvas: IFuVideoChatCanvas?, uid: Int, open: Boolean, channelType: FuChatChannelType) {
//("远端用户麦克风的状态")
videoChatCanvas?.apply {
(parentView() as FuSimpleVideoChatView).updateMicIcon(open)
}
}
override fun onLocalVideoOpen(uid: Int, roomId: String, channelType: FuChatChannelType) {
// "本地用户视频开启"
val privateVideView = FuSimpleVideoChatView(this@FuSimpleChatRoomActivity)
// privateVideView.updateVideoState(true)
privateVideView.setSwatchAvatarListener {
//切换头像
mChatRoomFragment.setPrivateChatOpenAvatarFlag(!mChatRoomFragment.getPrivateChatOpenAvatarFlag())
}
mBinding.layoutVideo.addViewByUid(uid, privateVideView)
mChatRoomFragment.setLocalVideoRenderView(privateVideView, roomId, uid)
//延时显示,防止首帧黑屏
privateVideView.postDelayed({
privateVideView.updateVideoState(true)
},500)
}
override fun onLocalVideoClose(videoChatCanvas: IFuVideoChatCanvas?, uid: Int, roomId: String, channelType: FuChatChannelType) {
//("本地用户视频关闭")
videoChatCanvas?.let {
//如果是私聊状态下,本地用户退出,就将所有video都移除(因为leaveChannel后其他人的Offline不一定能收到)
if (channelType == FuChatChannelType.Private) {
mBinding.layoutVideo.removeAllViews()
} else {
mBinding.layoutVideo.removeViewByUid(uid, it.parentView())
}
}
}
override fun onRemoteVideoOpen(uid: Int, roomId: String, channelType: FuChatChannelType) {
//("远端用户视频开启")
//TODO 处理动画的问题
if (channelType == FuChatChannelType.Private) {
mChatRoomFragment.updatePrivateState(true, uid)
}
val privateVideView = FuSimpleVideoChatView(this@FuSimpleChatRoomActivity)
privateVideView.updateVideoState(false)
mBinding.layoutVideo.addViewByUid(uid, privateVideView)
mChatRoomFragment.setRemoteVideoRenderView(privateVideView, roomId, uid)
}
override fun onRemoteVideoClose(videoChatCanvas: IFuVideoChatCanvas?, uid: Int, roomId: String, channelType: FuChatChannelType) {
//("远端用户视频关闭")
if (channelType == FuChatChannelType.Private) {
mChatRoomFragment.updatePrivateState(false, uid)
}
videoChatCanvas?.let {
mBinding.layoutVideo.removeViewByUid(uid, it.parentView())
}
}
override fun onRemoteVideoFirstFrame(videoChatCanvas: IFuVideoChatCanvas, uid: Int, roomId: String, channelType: FuChatChannelType) {
//TODO 首帧渲染的问题需要处理下
val videoChatView = videoChatCanvas.parentView() as FuSimpleVideoChatView
videoChatView.postDelayed({
videoChatView.updateVideoState(true)
},500)
}
override fun onCaptureVideoFrame(
data: ByteArray, frameType: Int, width: Int, height: Int, bufferLength: Int,
yStride: Int, uStride: Int, vStride: Int, rotation: Int, renderTimeMs: Long
) {
//("本地采集的视频帧")
mChatRoomFragment.modifyVideoData(width, height, data, yStride, uStride, vStride)
}
}在SDK初始化的时候设置cinemaRoomResourceConfig,参考多人互动 - 相应的assets资源放置好
/** * 描述:影院资源配置 * */class FuCinemaResourceConfig : IFuMosSceneResourceConfig { /** * 场景:chat/werewolf/scene/langrensha.bundle */
override fun getScenePath(): String = "chat/cinema/scene/simuyingyuan_jianmian.bundle" /** * 机位:chat/werewolf/scene/cam_langrensha_cj_rw.bundle */
override fun getCameraPath(): String = "chat/cinema/scene/Cinemachine_simuyingyuan.bundle" /** * 私聊场景的机位,不需要可设为“” */
override fun getPrivateCameraPath(): String = "" /** * 私聊场景的灯光:不需要可设为“” */
override fun getPrivateSceneLightPath(): String = "" /** * config/MobileRenderConfig.json */
override fun getMobileRenderConfigPath(): String = "config/MobileRenderConfig.json" /** * 寻路脚本配置 */
override fun getNavigationScriptPath(): String = "chat/cinema/script/simuyingyuan_script.bundle" /** * 动画Logic配置 */
override fun getLogicConfigPath(): String = "chat/cinema/script/graph/animLogic.json" /** * 动画Graph配置 */
override fun getGraphConfigPath(): String = "chat/cinema/script/graph/AnimGraph.json" /** * 脚本动画配置 */
override fun getScriptAnimConfigPath(): String = "chat/cinema/script/json/scriptAnimations.json" /** * 装修配置json */
override fun getDecorateConfigPath(): String = "chat/cinema/decorate/json/cinema_decotate.json" /** * 装修资源的根目录:chat/cinema/decorate */
override fun getDecorateRootPath(): String = "chat/cinema/decorate" /** * 单人动画配置表 * */
override fun getSingleAnimConfigPath(): String = "chat/cinema/single_interact_animation_config.json" /** * 双人动画配置表 */
override fun getDoubleAnimConfigPath(): String = "chat/cinema/double_interact_animation_config.json" /** * 动画资源根目录:chat/animation */
override fun getAnimationRootPath(): String = "chat/animation"
}app-mos/src/main/java/com/faceunity/app_mos/ui/cinema/FuCinemaRoomActivity.ktcom.faceunity.lib_mos.modules.ui.FuCinemaRoomFragmentlayout代码:(可参考demo中app-mos/src/main/res/layout/activity_fu_cinema_room.xml)
activity代码:
app-mos/src/main/java/com/faceunity/app_mos/ui/cinema/FuCinemaRoomActivity.kt...
import com.faceunity.lib_mos.FuMosKit
import com.faceunity.lib_mos.help.FUChatConstant
import com.faceunity.lib_mos.interfaces.FuMosResultListener
import com.faceunity.lib_mos.interfaces.IFuChatListener
import com.faceunity.lib_mos.interfaces.IFuVideoPlayListener
import com.faceunity.lib_mos.interfaces.NormalCallback
import com.faceunity.lib_mos.model.FuChatRoomTextMessage
import com.faceunity.lib_mos.modules.interact.decorate.FuDecorateBean
import com.faceunity.lib_mos.modules.interact.decorate.FuDecorateBundleBean
import com.faceunity.lib_mos.modules.rtcchat.FuChatChannelType
import com.faceunity.lib_mos.modules.ui.FuCinemaRoomFragment
.../** * 1.创建影院房间模板:FuCinemaRoomFragment */private val mChatRoomFragment: FuCinemaRoomFragment by lazy { FuCinemaRoomFragment() }
...
override fun onCreate(savedInstanceState: Bundle?) {
intent?.apply {
/**
* mRoomId房间号:加入房间的回调中会返回roomId
*/
mRoomId = getStringExtra(FUChatConstant.IntentKey.ROOMID).toString()
/**
* mUserInfo本地用户信息:加入房间的回调中会返回userInfo
*/
mUserInfo = getSerializableExtra(FUChatConstant.IntentKey.USERINFO) as UserInfo /** * 2.注入数据和回调 */
mChatRoomFragment.injectData(mRoomId, mUserInfo) /** * 3.注入监听 */
mChatRoomFragment.injectListener(mChatListener,mVideoPlayListener)
/**
* FPS 标记:可实现IFPSChecker,来打印或查看场景的fps,分析性能
*/
mChatRoomFragment.injectFPSChecker(MosFPSChecker()) /** * 4.绑定数据:用于界面恢复的时候 */
mChatRoomFragment.arguments = Bundle().apply{
putString(FUChatConstant.IntentKey.ROOMID,mRoomId)
putSerializable(FUChatConstant.IntentKey.USERINFO,mUserInfo)
} /** * 5.添加fragment到activity中 */
supportFragmentManager.beginTransaction().replace(R.id.fg_content, mChatRoomFragment).commitAllowingStateLoss()
...
}参考多人互动 普通语聊房-模板使用-基础功能
参考demo中:app-mos/src/main/java/com/faceunity/app_mos/ui/cinema/FuCinemaRoomActivity.kt
需要在初始化前配置好单人动作资源,参考多人互动 getSingleAnimConfigPath,见demo中:app-mos/src/main/assets/chat/cinema/single_interact_animation_config.json
使用方式参考多人互动 普通语聊房-基础功能-单人动作
发起密友申请:mChatRoomFragment.sendApplySecretFriends(userId,listener)
解除密友关系:mChatRoomFragment.releaseSecretFriends(userId)
com.faceunity.app_mos.ui.cinema.FuCinemaRoomActivity#applySecretFriendsDialog/** * 申请密友 */ fun sendApplySecretFriends(userId: Int, listener: FuMosResultListener) /** * 解除密友关系 */ fun releaseSecretFriends(userId: Int)
onReceiveSecretFriendsInvitation通过mChatRoomFragment.approveSubRoomSecretFriendApply(data.user.userId, data.applyId, true/false)对密友申请做出回应
/**
* 收到密友邀请
*/
fun onReceiveSecretFriendsInvitation(data:SecretFriendApplyNotification)
/**
* 密友申请通知
* 类型:notification
* event:social:secret_friend:apply_notification
*/
message SecretFriendApplyNotification {
/**
* 申请人信息
*/
common.UserInfo user = 1;
/**
* 申请唯一ID
*/
string apply_id = 2;
}需要在初始化前配置好双人动作资源,参考多人互动 getDoubleAnimConfigPath,见demo:app-mos/src/main/assets/chat/cinema/double_interact_animation_config.json
目前支持贴脸和摸头两种动作,分别可以站着、坐在木桩、坐在帐篷
两人相遇后avatar头顶会出现互动按钮,点击事件被触发会调IFuChatListener的回调:onTouchAvatarInteractBtn()
获取所有配置好的双人互动动作:mChatRoomFragment.getAllDoubleAnimations()
执行双人互动动作:mChatRoomFragment.playDoubleAnimationAction(type);会根据当前的状态播放不同动作:站着、坐在木桩、坐在帐篷
UI展示可参考demo:com.faceunity.app_mos.ui.cinema.FuCinemaRoomActivity#showAnimationDialog
/** * 双人互动 */data class FuAvatarDoubleInteractBean(
var type: String,//类型 TieTie\MoTou
var title: String,
var iconResName: String,//图片资源string名称
var distance: Float,//动画触发距离
var standAnim: FuAvatarInteractIntentionBean,//站着
var seatAnim: FuAvatarInteractIntentionBean,//坐在凳子上
var tentAnim: FuAvatarInteractIntentionBean//坐在帐篷中
)/** * @param active 主动方 * @param passive 被动方 */data class FuAvatarInteractIntentionBean(val active: FuAvatarInteractSideBean, var passive: FuAvatarInteractSideBean)/** * @param left 在左边 * @param right 在右边 */data class FuAvatarInteractSideBean(val left: FuAvatarInteractGenderBean, var right: FuAvatarInteractGenderBean) {
fun getAnimBean(isLeft: Boolean, gender: FuAvatarGender): FUChatAnimBean {
return (if (isLeft) left else right).getAnimBean(gender)
}
}发送消息:mChatRoomFragment.sendTextChatMessage
/** * 发送文字聊天消息 * @param content 消息内容 * @param listener 发送结果回调 */ fun sendTextChatMessage(content: String, listener: NormalCallback)
/**
* 处理软键盘输入
*/
private fun handleMsgInput(content: String) {
if (content.isEmpty()) {
ToastUtils.showNormalToast("请输入聊天内容")
return
}
mChatRoomFragment.sendTextChatMessage(content, object : NormalCallback {
override fun callback(success: Boolean) {
if (success) {
mBinding.etInput.text.clear()
mChatRoomFragment.getLocalUser()?.let {
val message =
ChatMessage.newBuilder().setContent(content).setOriginal(it.getUserInfo()).setType(Chatchannel.ChatType.User).build()
addChatMsgToView(FuChatRoomTextMessage(message))
}
}
}
})
}接收消息:实现IFuChatListener的方法onReceiveTextChatMessage
/** * 收到文字聊天消息 * @param data 消息内容 */ fun onReceiveTextChatMessage(data: FuChatRoomTextMessage)
文字聊天消息UI样式可参考demo中:app-mos/src/main/java/com/faceunity/app_mos/widget/ChatListView.kt
需要在初始化前配置好装修资源,参考多人互动 getDecorateConfigPath和getDecorateRootPath,见demo:app-mos/src/main/assets/chat/cinema/decorate/json/cinema_decotate.json
目前仅支持卡车喷漆和帐篷装饰物两种
开启/关闭装修房间的功能:mChatRoomFragment.modifyRoomDecorateEnabled(flag, null)
/** * 修改装修权限 */fun modifyRoomDecorateEnabled(enabled: Boolean, listener: FuMosResultListener?)
点击了装修按钮:回调IFuChatLisenter的onTouchDecorateBtn,可通过mChatRoomFragment.updateDecorateContentToServer(data.type, bundleBean.relativePath, listener)来更新装修内容
/** * 点击装修按钮 * @param data 装修的可选内容 */fun onTouchDecorateBtn(data:FuDecorateBean)/** * 更新装修配置 * 需要把目前所有的装修内容都同步一下 * @param bundleRelativePath bundle的相对路径 */fun updateDecorateContentToServer(type: String, bundleRelativePath: String, listener: FuMosResultListener? = null)/** * 描述:装修资源 * 根目录: * @see IFuSceneResourceConfig getDecorateRootPath() */data class FuDecorateBean( val type: String,//类型 Car\Tent val title:String, var position: FUCoordinate3DData,//装修按钮图标绘制的位置 var placeIconResName: String,//装修图标的图片资源的名称(drawable资源) var placeIconSize: FUAreaSizeData,//装修图标的尺寸 var bundlePref:FuDecorateBundleBean,//需要预制的bundle,比如车子换皮肤,需要先有一个车的bundle,然后在车身上更换不同的bundle(预制bundle不可以被替换掉) val bundleList: List//可切换的bundle ) /** * 描述: * * @param iconResName 对应显示的图片资源的名称(作为drawable资源) * @param relativePath 可切换的bundle(默认展示第一个)的相对路径 * @author hexiang on 2023/2/28 */ data class FuDecorateBundleBean(val iconResName: String, val relativePath: String)
房主通过mChatRoomFragment.setRoomVideoList(list)配置房间内可播放的视频列表,需要限制房主才能配置,否则会被覆盖
app-mos/src/main/assets/video0.mp4
app-mos/src/main/assets/video1.mp4
/**
* 房主设置房间视频列表
*/
private fun setVideoList() {
if (mChatRoomFragment.isMaster()) {
val list = ArrayList()
list.add(
VideoInfo.newBuilder().setUrl("video0.mp4").setSnapshot("https://mos-movies.oss-cn-hangzhou.aliyuncs.com/BGS07E01%402x.png").build()
)
list.add(
VideoInfo.newBuilder().setUrl("video1.mp4").setSnapshot("https://mos-movies.oss-cn-hangzhou.aliyuncs.com/BGS07E02%402x.png").build()
)
mChatRoomFragment.setRoomVideoList(list)
}
}展示视频列表:在onShowVideoList回调中展示相关UI,参考demo中:com.faceunity.app_mos.ui.cinema.FuCinemaRoomActivity#showVideoList
interface IFuVideoPlayListener { /** * 展示视频列表-表示用户点击了视频荧屏 * @param list 可播放的视频list */
fun onShowVideoList(list: List) /** * 开始播放 */
fun onStart(videoInfo: Video.VideoInfo, percent: Float, currentTime: Long) /** * 播放中 * @param percent 当前播放百分比 * @param currentTime 当前播放时间 */
fun onPlaying(videoInfo: Video.VideoInfo, percent: Float, currentTime: Long) /** * 暂停 */
fun onPause(videoInfo: Video.VideoInfo, percent: Float, currentTime: Long) /** * 恢复播放 */
fun onResume(videoInfo: Video.VideoInfo, percent: Float, currentTime: Long) /** * 播放完毕 */
fun onCompleted(videoInfo: Video.VideoInfo) /** * 关闭播放 */
fun onStop(videoInfo: Video.VideoInfo)
}房主可通过mChatRoomFragment.startVideo(data)来播放视频。
房主支持暂停/结束播放、拖动播放进度、切换影片等功能(相关UI操作封装在模板中),窗口播放和全屏播放所有成员可自行选择。
房主根据视频的播放状态对房间内麦克风进行控制,播放视频的时候禁止所有人麦克风;暂停、结束播放视频的时候恢复麦克风状态。参考demo:com.faceunity.app_mos.ui.cinema.FuCinemaRoomActivity#mVideoPlayListener
在SDK初始化的时候设置werewolfRoomResourceConfig,参考多人互动 - 相应的assets资源放置好
app-mos/src/main/assets/天亮了.mp3
app-mos/src/main/assets/天黑请闭眼.mp3
app-mos/src/main/assets/守卫请行动.mp3
app-mos/src/main/assets/开始放逐投票.mp3
app-mos/src/main/assets/狼人请行动.mp3
app-mos/src/main/assets/预言家请行动.mp3
app-mos/src/main/assets/chat/werewolf/kapai
/** * 描述:狼人杀资源配置 * */class FuWerewolfResourceConfig : IFuMosSceneResourceConfig { /** * 场景:chat/werewolf/scene/langrensha.bundle */
override fun getScenePath(): String = "chat/werewolf/scene/langrensha.bundle" /** * 相机 * [0是远景1是近景] */
override fun getCameraPath(): String = "chat/werewolf/scene/cam_langrensha_cj_rw.bundle" /** * 私聊场景的机位 */
override fun getPrivateCameraPath(): String = "chat/main/scene/cam_AXdemo_icon_texie.bundle" /** * 私聊场景的灯光 */
override fun getPrivateSceneLightPath(): String = "chat/main/scene/light_man.bundle" /** * config/MobileRenderConfig.json */
override fun getMobileRenderConfigPath(): String = "config/MobileRenderConfig.json" /** * 寻路脚本配置 */
override fun getNavigationScriptPath(): String = "chat/werewolf/script/langrensha_script.bundle" /** * 动画Logic配置 */
override fun getLogicConfigPath(): String = "chat/werewolf/script/graph/animLogic.json" /** * 动画Graph配置 */
override fun getGraphConfigPath(): String = "chat/werewolf/script/graph/AnimGraph.json" /** * 脚本动画配置 */
override fun getScriptAnimConfigPath(): String = "chat/werewolf/script/json/scriptAnimations.json"
override fun getDecorateConfigPath(): String = ""
override fun getDecorateRootPath(): String = "" /** * 单人动画配置表 * */
override fun getSingleAnimConfigPath(): String = "chat/cinema/single_interact_animation_config.json"
override fun getDoubleAnimConfigPath(): String = "" /** * 动画资源根目录:chat/animation */
override fun getAnimationRootPath(): String = "chat/animation"
}app-mos/src/main/java/com/faceunity/app_mos/ui/werewolf/FuWerewolfRoomActivity.ktcom.faceunity.lib_mos.modules.ui.FuWerewolfRoomFragmentlayout代码:(可参考demo中app-mos/src/main/res/layout/activity_fu_werewolf_room.xml)
activity代码:
app-mos/src/main/java/com/faceunity/app_mos/ui/werewolf/FuWerewolfRoomActivity.kt...
import com.faceunity.lib_mos.enums.GameStateLocal
import com.faceunity.lib_mos.help.FUChatConstant
import com.faceunity.lib_mos.interfaces.FuMosResultListener
import com.faceunity.lib_mos.interfaces.IFuChatListener
import com.faceunity.lib_mos.interfaces.NormalCallback
import com.faceunity.lib_mos.model.FuChatRoomTextMessage
import com.faceunity.lib_mos.modules.rtcchat.FuChatChannelType
import com.faceunity.app_mos.widget.video.FuWerewolfVideoChatView
import com.faceunity.lib_mos.modules.rtcchat.video.IFuVideoChatCanvas
import com.faceunity.lib_mos.modules.ui.FuWerewolfRoomFragment
import com.faceunity.lib_mos.modules.werewolf.IWerewolfGameListener
.../** * 1.创建狼人杀房间模板:FuWerewolfRoomFragment */private val mChatRoomFragment: FuWerewolfRoomFragment by lazy { FuWerewolfRoomFragment() }
...
override fun onCreate(savedInstanceState: Bundle?) {
intent?.apply { /** * mRoomId房间号:加入房间的回调中会返回roomId */
mRoomId = getStringExtra(FUChatConstant.IntentKey.ROOMID).toString() /** * mUserInfo本地用户信息:加入房间的回调中会返回userInfo */
mUserInfo = getSerializableExtra(FUChatConstant.IntentKey.USERINFO) as UserInfo /** * 2.注入数据 */
mChatRoomFragment.injectData(mRoomId, mUserInfo) /** * 3.注入监听 */
mChatRoomFragment.injectListener(mChatListener,mGameListener) /** * FPS 标记:可实现IFPSChecker,来打印或查看场景的fps,分析性能 */
mChatRoomFragment.injectFPSChecker(MosFPSChecker()) /** * 4.绑定数据:用于界面恢复的时候 */
mChatRoomFragment.arguments = Bundle().apply{
putString(FUChatConstant.IntentKey.ROOMID,mRoomId)
putSerializable(FUChatConstant.IntentKey.USERINFO,mUserInfo)
} /** * 5.添加fragment到activity中 */
supportFragmentManager.beginTransaction().replace(R.id.fg_content, mChatRoomFragment).commitAllowingStateLoss()
...
}参考多人互动 普通语聊房-模板使用-基础功能
参考demo中:app-mos/src/main/java/com/faceunity/app_mos/ui/werewolf/FuWerewolfRoomActivity.kt
参考多人互动 影院-模板使用-文字聊天
/** * 是否正在游戏中 */fun isGaming(): Boolean/** * 是否是游戏结束 */fun isGameOver(): Boolean/** * 获取游戏结果-游戏结束后 */fun getGameResult(listener: NormalCallback<Map>)
需要实现IWerewolfGameListener根据相应的状态来处理部分UI上的显示。参考demo中:com.faceunity.app_mos.ui.werewolf.FuWerewolfRoomActivity#mGameListener
/** * 描述:狼人杀游戏状态回调 * */interface IWerewolfGameListener { /** * 游戏初始状态-漫游 * 进入漫游状态需要将不需要的布局隐藏 */
fun onGameIdleState(state: GameStateLocal) /** * 游戏开始 * 房主的全员闭麦按钮需要隐藏:游戏过程中房主不能控制全员闭麦 */
fun onGameStart(state: GameStateLocal.Identify) /** * 进入黑夜状态 * 黑夜不允许显示按钮控制和文字输入框 */
fun onGameNight(state: GameStateLocal.Night) /** * 夜晚行动阶段 * 行动阶段:狼人才允许显示按钮控制和文字输入框 */
fun onGameNightAction(state: GameStateLocal.NightAction) /** * 夜晚等待别人行动 * 不允许显示按钮控制和文字输入框 */
fun onGameNightWait(state: GameStateLocal.NightWait) /** * 进入白天状态 */
fun onGameDay(state: GameStateLocal.Day) /** * 夜晚结算结果 */
fun onGameNightResult(state: GameStateLocal.NightResultShow) /** * 自己发言 * 显示按钮控制和文字输入框 */
fun onGameChat(state: GameStateLocal.Chat) /** * 等待别人发言 * 不允许显示按钮控制和文字输入框 */
fun onGameChatWait(state: GameStateLocal.ChatWait) /** * 投票阶段 * 不允许显示按钮控制和文字输入框 */
fun onGameVote(state: GameStateLocal.Vote)
/**
* 投票结果
*/
fun onGameVoteResult(state: GameStateLocal.VoteResultShow)
/**
* 游戏结束
*/
fun onGameOver(state: GameStateLocal.GameOver) /** * 游戏复盘 * 需要展示复盘布局,并更新游戏结果 * 房主需要 1、显示全员闭麦按钮 2、将所有人麦克风打开 3、显示结束复盘的按钮 */
fun onGameReview(state: GameStateLocal.Review)
}普通语聊房-模板使用-视频小窗功能-私聊狼人杀的自定义视频容器使用app-mos/src/main/java/com/faceunity/app_mos/widget/video/FuWerewolfVideoChatView.kt
mBinding.layoutGameReviewVideo
mBinding.layoutVideo
app-mos/src/main/java/com/faceunity/app_mos/widget/video/FuWerewolfChatContainer.ktapp-mos/src/main/java/com/faceunity/app_mos/widget/video/WerewolfReviewVideoContainer.ktcom.faceunity.app_mos.ui.werewolf.FuWerewolfRoomActivity#mChatListeneroverride fun onLocalVideoOpen(uid: Int, roomId: String, channelType: FuChatChannelType) {
// "本地用户视频开启"
val privateVideView = FuWerewolfVideoChatView(this@FuWerewolfRoomActivity)
privateVideView.updateVideoState(false)
privateVideView.setSwatchAvatarListener {
//切换头像
mChatRoomFragment.setPrivateChatOpenAvatarFlag(!mChatRoomFragment.getPrivateChatOpenAvatarFlag())
}
mChatRoomFragment.getChatUsers()[uid]?.let { userBean ->
privateVideView.updateNumber(userBean.uid, userBean.getUserInfo().werewolfInfo.playerNumber)
}
if (mChatRoomFragment.isGameOver()) { //复盘--需要确定身份和死亡原因
addVideoToWerewolfGameReview(uid, privateVideView)
} else {
if (channelType==FuChatChannelType.Public){ //普通发言
mBinding.layoutVideo.addViewByUidToCenter(uid, privateVideView)
}else{ //狼人行动环节聊天
mBinding.layoutVideo.addViewByUid(uid, privateVideView)
}
}
mChatRoomFragment.setLocalVideoRenderView(privateVideView, roomId, uid)
//延时显示,防止首帧黑屏
privateVideView.postDelayed({
privateVideView.updateVideoState(true)
},500)
if (channelType==FuChatChannelType.Private){
showPrivateBtn()
}
}
override fun onLocalVideoClose(videoChatCanvas: IFuVideoChatCanvas?, uid: Int, roomId: String, channelType: FuChatChannelType) {
//("本地用户视频关闭")
if (channelType==FuChatChannelType.Private){
hidePrivateBtn()
}
videoChatCanvas?.let {
//如果是私聊状态下,本地用户退出,就将所有video都移除(因为leaveChannel后其他人的Offline不一定能收到)
if (channelType == FuChatChannelType.Private) {
mBinding.layoutVideo.removeAllViews()
} else {
mBinding.layoutVideo.removeViewByUid(uid, it.parentView())
mBinding.layoutGameReviewVideo.removeViewByUid(uid, it.parentView())
}
}
}
override fun onRemoteVideoOpen(uid: Int, roomId: String, channelType: FuChatChannelType) {
//("远端用户视频开启")
if (channelType == FuChatChannelType.Private) {
mChatRoomFragment.updatePrivateState(true, uid)
}
val privateVideView = FuWerewolfVideoChatView(this@FuWerewolfRoomActivity)
privateVideView.updateVideoState(false)
mChatRoomFragment.getChatUsers()[uid]?.let { userBean ->
privateVideView.updateNumber(userBean.uid, userBean.getUserInfo().werewolfInfo.playerNumber)
}
if (mChatRoomFragment.isGameOver()) { //复盘--需要确定身份和死亡原因
addVideoToWerewolfGameReview(uid, privateVideView)
} else {
if (channelType==FuChatChannelType.Public){ //普通发言
mBinding.layoutVideo.addViewByUidToCenter(uid, privateVideView)
}else{ //狼人行动环节聊天
mBinding.layoutVideo.addViewByUid(uid, privateVideView)
}
}
mChatRoomFragment.setRemoteVideoRenderView(privateVideView, roomId, uid)
}
override fun onRemoteVideoClose(videoChatCanvas: IFuVideoChatCanvas?, uid: Int, roomId: String, channelType: FuChatChannelType) {
//("远端用户视频关闭")
videoChatCanvas?.let {
mBinding.layoutVideo.removeViewByUid(uid, it.parentView())
mBinding.layoutGameReviewVideo.removeViewByUid(uid, it.parentView())
}
}
private fun addVideoToWerewolfGameReview(uid: Int, privateVideView: FuWerewolfVideoChatView) {
mChatRoomFragment.getGameResult(object : NormalCallback<Map> {
override fun callback(mapGameResult: Map) {
mapGameResult[uid]?.let { map ->
val gameIdentity = map.identity
val outType = if (map.dead) map.reason else null
privateVideView.updateGameIdentify(gameIdentity)
privateVideView.updateGameOutReason(outType)
if (gameIdentity != Werewolf.GameIdentity.Werewolf) {
mBinding.layoutGameReviewVideo.addLeftViewByUid(uid, privateVideView)
} else {
mBinding.layoutGameReviewVideo.addRightViewByUid(uid, privateVideView)
}
}
}
})
}