1. 形象生成

      形象生成依赖组件FuBuilderKit,和云服务组件FUCloudKit

      初始化FUBuilder组件和云服务

    /**
     * 初始化生成组件
     */
    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")
            }
        }
    }

      人脸有效性检测

    人脸有效性检测意为在生成形象之前需要检测图片中人脸数据是否有效,依赖组件FuBuilderKit
    /**
     * 图片人脸检测
     * @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)
        )
    }
具体人脸检测结果校验请参考demo实现

图片在线生成形象

/**
 * 通过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
    )

}
具体实现请参考demo
  1. 多人互动

一、快速开始

  1. 准备

  1. 获取配置信息

请参考AvatarX SDK快速开始快速开始->快速跑通->必要信息配置来获取自己的证书、云平台配置信息、Mos云服务地址。
需要购买所需要的房间模板,否则鉴权不通过无法使用
内容示例如下:
1、证书文件:authpack.java
2、云平台配置信息:
    baseUrl = "  https://avatarxapi.faceunity.com  ",
    apiKey = "41rCEUV1vBvP63hryPo6JW",
    apiSecret = "xrbBu3gy55253xbEJqSkS5",
    appId = "WJ6oPyrh36PvBv1VUECr14"
3、Mos云服务地址:ws://47.98.208.52:8000
  1. 集成MOS SDK

  1. gradle环境配置

在项目的 /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' }
    }
}
  1. 添加依赖

Mos sdk支持的minSdkVersion为21
/Gradle Scripts/build.gradle(Module:.app) 中添加如下依赖:
...
dependencies {
 ... // 1.0.0 为具体版本号 // 最新版本号请咨询
 implementation 'com.faceunity.mos:full:1.0.0'
}
  1. assets资源配置

  • 如使用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
}
  • MOS功能的资源导入:
    •   按需导入,app-mos/src/main/assets/chat
    • assets/chat/animation:为mos功能中所需要的动画资源

    • assets/chat/main:普通语聊房间的场景配置资源

    • assets/chat/cinema:私人影院房间的场景配置资源

    • assets/chat/werewolf:狼人杀房间的场景配置资源

  1. 添加项目权限

  • 基本权限配置

根据场景需要,在 /app/src/main/AndroidManifest.xml 文件中添加如下代码,获取相应的设备权限

    

企业微信截图_16849408378656.png

  • 动态权限申请

对于Android6.0及以上设备需要处理动态权限申请,参考demo中: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
    )
}
注意:targetSdkVersion>30以后需要在AndroidManifest.xml文件中增加如下配置
        ...        
              ...
  1. MOS SDK初始化

建议在Application中初始化
  1. 相关依赖组件初始化

  1. Render组件初始化

参考AvatarX SDK文档:快速开始 快速开始-环境初始化-SDK鉴权初始化
  1. 资源管理组件初始化

参考AvatarX SDK文档:快速开始 快速开始-环境初始化-初始化资源管理组件
  1. 云服务组件初始化

参考AvatarX SDK文档:快速开始  快速开始-环境初始化-初始化云服务组件
  1. 功能初始化

  • @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()
)
  1. 日志初始化

/** * mos日志等级配置,不配置-》默认OFF不打印日志 * VERBOSE(0), DEBUG(1), INFO(2), WARN(3), ERROR(4), OFF(5); */FuMosKit.getInstance().initMosLog(FuMosLogger.LogLevel.DEBUG)

二、基础功能

以下均使用FuMosKit.getInstance()来调用
  1. 登录

1.登录成功后才能使用其他功能
2.在做好前面初始化工作的前提下,需要在使用所有mos功能的前面进行登录,如果在进入相应房间然后退出房间,还需要使用mos的网关功能,需要检查是否登录确保是登录成功状态
  1. 登录

/** * 登录 * @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
  1. 更新用户信息

/** * 更新用户信息 * @param nickname 昵称 * @param avatarId Avatar形象id * @param avatarPngUrl 头像 */fun updateBaseUserInfo(nickname: String, avatarId: String, avatarPngUrl: String, listener: FuMosResultListener?)
  1. 示例

请参考demo中com.faceunity.app_mos.ui.home.FuMosHomeViewModel
fun 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()
            }

        })
}
  1. 退出

登录后,想要退出,需要调用退出接口
FuMosKit.getInstance().leave()
  1. 创建房间

需要购买所需要的房间模板,否则鉴权不通过无法使用
  1. 方法

/** * 创建房间 * @param roomType 房间类型 * @param rangeMax 私聊范围识别圈的大小,默认200 * @param birthPositions 出生位置 * @param callbackListener 返回房间信息,根据返回的房间信息加入房间 */fun createRoom(
    roomType: Common.RoomType,
    rangeMax: Float = 200f,
    birthPositions: List,
    callbackListener: FuMosResultListener)
  1. 参数

  • 参数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)
)
  1. 示例

请参考demo中:com.faceunity.app_mos.ui.home.FuMosHomeViewModel
创建房间成功后需要根据返回的roomInfo加入房间,请参考多人互动 基础功能->加入房间->获取房间列表后选择房间加入->根据房间信息加入房间
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)
        }

    })
}
  1. 加入房间

对于Android6.0及以上设备在加入房间前:需要动态申请权限
参考demo: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)
  1. 通过房间号加入房间

需要参数roomId、gender
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)
        }

    })
  1. 获取房间列表后选择房间加入

  1. 查询可用房间

/** * 查询名下所有房间 * @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)
    }

})
  1. 根据房间信息加入房间

查询到RoomInfo后
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)
            }

        })
}
加入房间的成功回调fun onSuccess(data: FuJoinRoomResponse)中会返回本地用户信息userInfo
data class FuJoinRoomResponse(
    val roomId: String,
    val roomType: Common.RoomType,
    val userInfo: Common.UserInfo?,
    val success: Boolean,
    val msg: String? = null
)
  1. 通过邀请加入房间

仅在房间模板外部有效
  • 好友邀请的通知

在登录时,多人互动 基础功能->登录->登录会注入好友邀请的监听@param friendInvitationListener 好友邀请的监听(仅接收login后,到createRoom、joinRoom之前的邀请通知)
/** *  好友邀请信息 *  @param inviteInfo 邀请信息 *  @param roomInfo 邀请的房间信息 */data class FuRoomFriendInvitationInfo(val inviteInfo: FriendInvitationNotification, val roomInfo: RoomInfo)
  • 对好友邀请通知的处理

参考demo:com.faceunity.app_mos.ui.home.FuMosHomeViewModel#handleInvitation
  • 根据好友邀请信息中的roomInfo加入房间,参考多人互动 基础功能->加入房间->获取房间列表后选择房间加入->根据房间信息加入房间

三、房间模版

  1. 普通语聊房

  1. 场景配置

  • 在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"

}
  1. 模板使用

参考demo中:app-mos/src/main/java/com/faceunity/app_mos/ui/simple/FuSimpleChatRoomActivity.kt
  1. 界面显示、初始化

房间模板采用Fragment的形式实现,采用动态加载Fragment的方式载入,影院房间的模板为com.faceunity.lib_mos.modules.ui.FuSimpleChatRoomFragment
  • layout代码:(可参考demo中app-mos/src/main/res/layout/activity_fu_simple_chat_room.xml

        

                                                
  • activity代码:

可参考demo中:
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()
    ...
}
  1. 基础功能

  1. 实现IFuChatListener基本方法
根据场景需要,可以选择实现IFuChatListener的基本方法
/** * 本地公聊频道麦克风是否可以自由控制 * 可以控制麦克风按钮是否可以点击 * @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()
    }
    
    ...
}
  1. 公聊全员闭麦-房主
  • 房主可调用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("已开启全员闭麦")
    }
}
  1. 公聊麦克风控制
  • 调用方法mChatRoomFragment.publicMic(openMicro),返回值true、false表示是否成功的控制麦克风

/** * 控制公聊麦克风 * @param open true:开启麦克风 * @return 是否成功 */fun publicMic(open: Boolean)/** * 获取公聊麦克风状态 */fun getPublicMicState(): Boolean
  1. 公聊扬声器控制
  • 调用方法mChatRoomFragment.publicAudio(open)

/** * 控制公聊听筒 * @param open true:开启听筒 * @return 是否成功 */fun publicAudio(open: Boolean): Boolean 


/** * 公聊听筒状态 */fun getPublicAudioState(): Boolean
  1. 私聊麦克风控制
  • 调用方法mChatRoomFragment.privateMic(open)

/** * 控制私聊麦克风 * @param open true:开启麦克风 */fun privateMic(open: Boolean): Boolean/** * 私聊麦克风状态 */fun getPrivateMicState(): Boolean
  1. 私聊扬声器控制
  • 调用方法mChatRoomFragment.privateAudio(open)

/** * 控制私聊听筒 * @param open true:开启听筒 */fun privateAudio(open: Boolean): Boolean/** * 获取私聊听筒状态 */fun getPrivateAudioState(): Boolean
  1. 查询大厅用户
  • 在房间内调用mChatRoomFragment.queryHomeUsers(searchContent, listener)

  • 支持精准搜索nickname或者user_id

/** * 查询大厅内用户 * 目前限制50人 * @param searchContent 搜索内容(nickname或者user_id) */fun queryHomeUsers(searchContent: String?, listener: FuMosResultListener)
  1. 邀请好友
  • 通过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)
  • 被邀请的好友会收到邀请通知,参考多人互动 基础功能-通过邀请加入房间

  1. 查询当前房间内用户
  • 调用mChatRoomFragment.queryRoomUsersExcludeLocal可查询当前房间内所有用户列表(不包括本地用户)

/** * 查询当前房间中所有用户-包括本地用户 */fun queryRoomUsers(listener: FuMosResultListener)/** * 查询当前房间中所有用户-不包括本地用户 */fun queryRoomUsersExcludeLocal(listener: FuMosResultListener)
  1. 踢人
  • 调用mChatRoomFragment.queryRoomUsersExcludeLocal可查询当前房间内所有用户列表后,可得到想踢的人的用户信息,然后调用mChatRoomFragment.removeUserFromRoom(userId)将指定userId的用户踢出房间

/**
 * 踢人
 * @param userId 要踢出的用户id
 */
fun removeUserFromRoom(userId: Int)
  • 被踢出的用户会收到相应通知,执行退出房间的逻辑,收到onLeave回调,参考多人互动 实现IFuChatListener基本方法-onLeave

  1. 退出房间、关闭房间
  • 房主和非房主都是调用mChatRoomFragment.leaveRoom()来退出房间的,要早收到onLeave回调再销毁页面参考多人互动 实现IFuChatListener基本方法-onLeave

  1. 单人动作
  • 需要在初始化前配置好单人动作资源,参考多人互动 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)
  1. 视频小窗功能-私聊

需要私聊功能的话,需要实现IFuChatListener的以下方法
/** * 进入了私聊房间(两个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
) {
}

  1. 本地用户进入/离开私聊
  • onJoinPrivate、onLeavePrivate表示本地用户进入或离开私聊,可以用来显示/隐藏私聊麦克风、扬声器控制按钮

  1. 本地用户开启/关闭视频
  • 通过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())
                }
            }
        }
  1. 本地用户采集的视频数据预处理
  • 通过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)
        }
  1. 远端用户开启/关闭视频
  • 通过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)
        }
    }
  1. 影院

  1. 场景配置

  • 在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"

}


  1. 模板使用

参考demo中:app-mos/src/main/java/com/faceunity/app_mos/ui/cinema/FuCinemaRoomActivity.kt
  1. 界面显示、初始化

房间模板采用Fragment的形式实现,采用动态加载Fragment的方式载入,普通语聊房的模板为com.faceunity.lib_mos.modules.ui.FuCinemaRoomFragment
  • layout代码:(可参考demo中app-mos/src/main/res/layout/activity_fu_cinema_room.xml

                                                        
  • activity代码:

可参考demo中:
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()
    ...
}
  1. 基础功能

  • 参考多人互动 普通语聊房-模板使用-基础功能

  • 参考demo中:app-mos/src/main/java/com/faceunity/app_mos/ui/cinema/FuCinemaRoomActivity.kt

  1. 单人互动
  • 需要在初始化前配置好单人动作资源,参考多人互动  getSingleAnimConfigPath,见demo中:app-mos/src/main/assets/chat/cinema/single_interact_animation_config.json

  • 使用方式参考多人互动 普通语聊房-基础功能-单人动作

  1. 双人互动

密友之间才支持双人互动
  1. 密友
可通过多人互动 查询当前房间内用户
  • 发起密友申请:mChatRoomFragment.sendApplySecretFriends(userId,listener)

  • 解除密友关系:mChatRoomFragment.releaseSecretFriends(userId)

参考demo中:com.faceunity.app_mos.ui.cinema.FuCinemaRoomActivity#applySecretFriendsDialog
/**
 * 申请密友
 */
fun sendApplySecretFriends(userId: Int, listener: FuMosResultListener)

/**
 * 解除密友关系
 */
fun releaseSecretFriends(userId: Int)
  • 收到密友邀请:实现IFuChatListener的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;
}
  1. 动作执行
  • 需要在初始化前配置好双人动作资源,参考多人互动 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)
    }
}
  1. 文字聊天

  • 发送消息: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

  1. 装修功能

  • 需要在初始化前配置好装修资源,参考多人互动 getDecorateConfigPathgetDecorateRootPath,见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)
  1. 共享视频

  1. 配置可播放列表(目前支持本地视频)
  • 房主通过mChatRoomFragment.setRoomVideoList(list)配置房间内可播放的视频列表,需要限制房主才能配置,否则会被覆盖

  • 目前支持本地视频:视频放在assets文件夹下,可根据需要自由替换(文件名根配置的url保持一致即可)
    • 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)
    }
}
  1. 实现IFuVideoPlayListener
  • 展示视频列表:在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)
    
}
  1. 视频播放
  • 房主可通过mChatRoomFragment.startVideo(data)来播放视频。

  • 房主支持暂停/结束播放、拖动播放进度、切换影片等功能(相关UI操作封装在模板中),窗口播放和全屏播放所有成员可自行选择。

  • 房主根据视频的播放状态对房间内麦克风进行控制,播放视频的时候禁止所有人麦克风;暂停、结束播放视频的时候恢复麦克风状态。参考demo:com.faceunity.app_mos.ui.cinema.FuCinemaRoomActivity#mVideoPlayListener

  1. 狼人杀

  1. 场景配置

  • 在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"

}


  1. 模板使用

参考demo中:app-mos/src/main/java/com/faceunity/app_mos/ui/werewolf/FuWerewolfRoomActivity.kt
  1. 界面显示、初始化

房间模板采用Fragment的形式实现,采用动态加载Fragment的方式载入,普通语聊房的模板为com.faceunity.lib_mos.modules.ui.FuWerewolfRoomFragment
  • layout代码:(可参考demo中app-mos/src/main/res/layout/activity_fu_werewolf_room.xml)

                                                                                                
  • activity代码:

可参考demo中:
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()

    ...
}
  1. 基础功能

  • 参考多人互动 普通语聊房-模板使用-基础功能

  • 参考demo中:app-mos/src/main/java/com/faceunity/app_mos/ui/werewolf/FuWerewolfRoomActivity.kt

  1. 文字聊天

  1. 狼人杀游戏

  1. 游戏查询接口
/** * 是否正在游戏中 */fun isGaming(): Boolean/** * 是否是游戏结束 */fun isGameOver(): Boolean/** * 获取游戏结果-游戏结束后 */fun getGameResult(listener: NormalCallback<Map>)
  1. 狼人杀游戏状态回调IWerewolfGameListener
  • 需要实现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)

}
  1. 游戏过程中视频聊天框
参考多人互动 普通语聊房-模板使用-视频小窗功能-私聊
与普通语聊房不同的是视频容器需要展示更多内容:比如号码牌、身份等
  • 自定义视频容器
    • 狼人杀的自定义视频容器使用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.kt
    • 复盘阶段app-mos/src/main/java/com/faceunity/app_mos/widget/video/WerewolfReviewVideoContainer.kt
参考demo中:com.faceunity.app_mos.ui.werewolf.FuWerewolfRoomActivity#mChatListener
override 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)
                }
            }
        }
    })
}

四、拓展功能(另外排期)

5.1 互动
5.2 音视频交互


< 上一页: 常见问题
下一页: 扩展功能 >