实现融合场景

  • Android

更新时间不可用

了解声网解决方案在不同平台上的融合场景方式,包括接入流程、关键配置和平台差异。

本文介绍如何实现元语聊和元直播的融合场景。

示例项目

声网在 GitHub 上提供开源 Agora-MetaWorld 示例项目供你参考。

如果你还需了解 Unity 部分的工程文件和功能指南,请联系 sales@shengwang.cn 获取。

实现元语聊和元直播融合场景

完成集成声网 Meta SDK 后,你可以参考本节实现元语聊和元直播的融合场景。

下图展示实现元语聊和元直播融合场景的 API 调用时序:

实现步骤需用到如下类:

  • RtcEngine 类:提供实时音视频功能的核心类。
  • IMetaService 类:提供 Meta 服务的核心类。可用于获取场景资源列表、下载场景资源、删除本地场景资源等场景资源管理,还可用于创建 IMetaScene。
  • IMetaScene 类:场景资源相关操作。
  • ILocalUserAvatar 类:包含在 IMetaScene 中,生命周期和 IMetaScene 相同,用于设置虚拟形象(Avatar)。
  • IMetaServiceEventHandler 类:IMetaService 的异步方法的事件回调类。
  • IMetaSceneEventHandler 类:IMetaScene 的异步方法的事件回调类。

1 初始化 RTC 引擎和 Meta 服务

调用 RtcEngine 类的 create 创建 RtcEngine。调用 IMetaService 类的 create 和 initialize 创建并初始化 IMetaService。

初始化 IMetaService 时,需要在 MetaServiceConfig 里设置如下重要的字段:

  • mRtcEngine:通过 create 方法创建的 RtcEngine 实例。
  • mAppId:在声网控制台获取的 App ID。详见开通元语聊服务。
  • mUserId:登录声网 RTM 系统的用户 ID。推荐取值详见 MetaServiceConfig。
  • mRtmToken:用于登录声网 RTM 系统的动态密钥。开启动态鉴权后可用。详见生成 Token。
  • mLocalDownloadPath:场景资源下载到本地的保存路径。
  • mEventHandler:IMetaService 的回调事件句柄。

声网项目有两种 Token 和 UID,请不要搞混淆:

Java
// 设置 RtcEngine 配置
RtcEngineConfig rtcConfig = new RtcEngineConfig();
rtcConfig.mContext = context;
// 声网项目的 App ID,从控制台获取
rtcConfig.mAppId = KeyCenter.APP_ID;
rtcConfig.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
// 监听 RTC 引擎回调事件
rtcConfig.mEventHandler = new IRtcEngineEventHandler() {
    @Override
    public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
        Log.d(TAG, String.format("onJoinChannelSuccess %s %d", channel, uid));
    }

    @Override
    public void onUserOffline(int uid, int reason) {
        Log.d(TAG, String.format("onUserOffline %d %d ", uid, reason));
    }

    @Override
    public void onAudioRouteChanged(int routing) {
        Log.d(TAG, String.format("onAudioRouteChanged %d", routing));
    }

    @Override
    public void onUserJoined(int uid, int elapsed) {
        Log.d(TAG, "onUserJoined uid=" + uid);
    }

    @Override
    public void onFirstRemoteVideoDecoded(int uid, int width, int height, int elapsed) {
        Log.d(TAG, "onFirstRemoteVideoDecoded uid=" + uid + ",width=" + width + ",heigh=" + height + ",elapsed=" + elapsed);
    }
};
rtcConfig.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT);

// 创建 RtcEngine
rtcEngine = RtcEngine.create(rtcConfig);
rtcEngine.setParameters("{\"rtc.enable_debug_log\":true}");

// 开启音视频模块并设置音频属性和路由
rtcEngine.enableAudio();
rtcEngine.enableVideo();
rtcEngine.setAudioProfile(
        Constants.AUDIO_PROFILE_DEFAULT, Constants.AUDIO_SCENARIO_GAME_STREAMING
);
rtcEngine.setDefaultAudioRoutetoSpeakerphone(true);

// 设置场景资源的下载路径
scenePath = context.getExternalFilesDir("").getPath();
{
   // 创建 IMetaService
    metaService = IMetaService.create();

    MetaServiceConfig config = new MetaServiceConfig() {{
        // RtcEngine 实例
        mRtcEngine = rtcEngine;
        // 声网项目的 App ID,从控制台获取
        mAppId = KeyCenter.APP_ID;
        // 声网 RTM(云信令)Token,保障安全
        // 声网项目有 RTC Token 和 RTM Token,不要搞混淆
        mRtmToken = KeyCenter.RTM_TOKEN;
        mLocalDownloadPath = scenePath;
        // 声网 RTM(云信令)UID,用户 ID,标志用户身份
        // 声网项目有 RTC UID 和 RTM UID,不要搞混淆
        mUserId = KeyCenter.RTM_UID;
        mEventHandler = MetaContext.this;
    }};
    // 初始化 IMetaService
    ret += metaService.initialize(config);
    Log.i(TAG, "launcher version=" + metaService.getLauncherVersion(context));
}

2 获取并下载场景资源

调用 IMetaService 类的 getSceneAssetsInfo 获取场景资源,并通过 IMetaServiceEventHandler 类的 getSceneAssetsInfo 回调监听获取场景资源时的事件。

调用 IMetaService 类的 downloadScene 获取场景资源,并通过 IMetaServiceEventHandler 类的 onDownloadSceneAssetsProgress 回调监听获取场景资源时的事件。

Java
// 获取场景资源
public boolean getSceneInfos() {
    return metaService.getSceneAssetsInfo() == Constants.ERR_OK;
}

// 监听获取场景资源的回调事件
@Override
public void onGetSceneAssetsInfoResult(MetaSceneAssetsInfo[] metaSceneAssetsInfos, int errorCode) {
}

// 下载场景资源
public boolean downloadScene(MetaSceneAssetsInfo sceneInfo) {
    return metaService.downloadSceneAssets(sceneInfo.mSceneId) == Constants.ERR_OK;
}

// 监听下载场景资源的回调事件
@Override
public void onDownloadSceneAssetsProgress(long sceneId, int progress, int state) {
}

3 创建场景

调用 IMetaService 类 createScene 创建 IMetaScene,并在 MetaSceneConfig 中设置场景配置信息。为增加直播趣味性,声网推荐你开启面部捕捉,使用同步人脸表情的 Avatar 形象。你需要在 MetaSceneConfig 中设置 mEnableFaceCapture 为 true,并在 mFaceCaptureAppId 和 mFaceCaptureCertificate 中传入面部捕捉插件的 ID 和 Key。

通过 IMetaServiceEventHandler 类的 onCreateSceneResult 和 onConnectionStateChanged 回调监听创建场景和连接状态的事件。

Java
// 配置场景信息
MetaSceneConfig sceneConfig = new MetaSceneConfig();
sceneConfig.mActivityContext = activityContext;
// 设置是否开启面部捕捉
// 融合场景中,建议开启面部捕捉
sceneConfig.mEnableFaceCapture = true;
// 传入面部捕捉插件的 App ID 和 Certificate
sceneConfig.mFaceCaptureAppId = KeyCenter.FACE_CAP_APP_ID;
sceneConfig.mFaceCaptureCertificate = KeyCenter.FACE_CAP_APP_KEY;
int ret = -1;
if (metaScene == null) {
    // 创建场景
    ret = metaService.createScene(sceneConfig);
}

// 监听创建场景的回调事件
@Override
public void onCreateSceneResult(IMetaScene scene, int errorCode) {
    Log.i(TAG, "onCreateSceneResult errorCode: " + errorCode);
    metaScene = scene;
    // 获取 ILocalUserAvatar 对象
    localUserAvatar = metaScene.getLocalUserAvatar();
}

// 监听连接状态
@Override
public void onConnectionStateChanged(int state, int reason) {
    Log.d(TAG, "onConnectionStateChanged state=" + state + ",reason=" + reason);
    if (state == ConnectionState.META_CONNECTION_STATE_ABORTED) {
        setCurrentScene(MetaConstants.SCENE_NONE);
        resetRoleInfo();
        leaveScene();
    }
}

4 设置用户信息并进入场景

要完成进入场景的操作,参考如下步骤:

  1. 调用 ILocalUserAvatar 类的 setUserInfo 和 setModelInfo 设置用户的基本信息和虚拟形象(Avatar)的模型信息。
  2. 调用 ILocalUserAvatar 类的 setExtraInfo 设置用户的捏脸、换装信息。
  3. 调用 IMetaScene 类的 enterScene 进入场景,并通过 config 设置配置信息。
  4. 通过 IMetaSceneEventHandler 类的 onEnterSceneResult 回调监听进入场景的结果。
Java
public void enterScene() {
    if (null != localUserAvatar) {
        // 设置用户的基本信息
        localUserAvatar.setUserInfo(userInfo);
        // 设置用户的虚拟形象模型信息
        // 模型信息的 mBundleType 需设为 2(BUNDLE_TYPE_AVATAR)
        localUserAvatar.setModelInfo(modelInfo);
        if (null != roleInfo) {
                // 设置用户的自定义捏脸、换装信息
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("2dbg", "");
                jsonObject.put("avatar", roleInfo.getAvatarType());
                jsonObject.put("dress", roleInfo.getDressResourceMap().values().toArray((new Integer[0])));
                jsonObject.put("face", roleInfo.getFaceParameterResourceMap().values().toArray((new FaceParameterItem[0])));
                localUserAvatar.setExtraInfo(jsonObject.toJSONString().getBytes());
            }
    }
    if (null != metaScene) {
        // 监听 IMetaScene 的事件回调
        metaScene.addEventHandler(MetaContext.getInstance());
        // 设置进入场景时的配置信息
        EnterSceneConfig config = new EnterSceneConfig();
        // 场景资源渲染时所需要的视图,Android 上使用原生 TextureView
        config.mSceneView = this.sceneView;
        // 进入场景的房间名称
        config.mRoomName = KeyCenter.CHANNEL_ID;
        // 内容中心对应的 ID
        if (null != sceneInfo) {
            config.mSceneId = this.sceneInfo.mSceneId;
        }
        // 设置进入场景的 ID 和加载场景资源的路径
        if (isEnableLocalSceneRes) {
            config.mSceneId = 0;
            config.mScenePath = scenePath + "/" + getSceneId(); // 场景的加载路径
        }
        /*
         * 仅为示例格式,具体格式以项目实际为准
         *   "extraInfo":{
         *     "sceneIndex":0  // 0 为默认场景,在这里指咖啡厅
         *   }
         */
        EnterSceneExtraInfo extraInfo = new EnterSceneExtraInfo();
        extraInfo.setSceneIndex(MetaConstants.SCENE_DRESS);

        // 设置加载场景资源时需要的额外自定义信息,只支持字符串
        // 在这里指设置 sceneIndex
        // 在业务逻辑中包含多个场景的情况下,你可以用 sceneIndex 来区分不同的场景,Unity 场景脚本可以根据 sceneIndex 来确定进入哪个场景,并执行相应的逻辑
        config.mExtraInfo = JSONObject.toJSONString(extraInfo).getBytes();
        // 进入场景
        metaScene.enterScene(config);
    }
}

// 监听进入场景的回调事件
@Override
public void onEnterSceneResult(int errorCode) {
    Log.d(TAG, String.format("onEnterSceneResult %d", errorCode));
}

注意

  • 本小节展示在进入场景前设置用户和模型信息,如需在进入场景后变更设置,需调用 applyInfo 使设置生效。
  • 声网 Meta SDK 支持自定义装扮捏脸功能,详见换装和捏脸。

5 加入频道并开启直播

进入场景后,你需要在场景中添加一个视图,将主播端 Avatar 形象的视频流发布到 RTC 频道中,使 3D 场景中的用户都能看到直播。参考如下步骤:

  1. 调用 RtcEngine 类的 setupLocalVideo 初始化本地视图,用于本地预览。
  2. 调用 RtcEngine 类的 joinChannel 使主播加入 RTC 频道。
  3. 调用 IMetaScene 类的 addSceneView 在场景中添加一个视图,用于在频道内发布主播的 Avatar 形象。
  4. 通过 IMetaSceneEventHandler 类的 onAddSceneViewResult 回调监听添加场景显示视图的结果。
  5. 调用 IMetaScene 类的 enableSceneVideoCapture 并将 enable 设置为 true 开启场景渲染画面捕获,发布主播的 Avatar 形象到 RTC 频道。

注意

发送 Avatar 视频前,请确保 MetaSceneConfig 中已设置开启面部捕捉。

Java
// 创建 VideoCanvas 对象,用于设置本地视频画面的显示属性
VideoCanvas videoCanvas = new VideoCanvas(mLocalPreviewSurfaceView, VideoCanvas.RENDER_MODE_HIDDEN, 0);
// 设置显示摄像头采集后的原始画面
videoCanvas.position = Constants.VideoModulePosition.VIDEO_MODULE_POSITION_POST_CAPTURER_ORIGIN;
// 设置本地视频画面的参数,并开启本地视频预览
rtcEngine.setupLocalVideo(videoCanvas);
// 设置用户角色为主播
rtcEngine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER);
 
// 加入频道
public void joinChannel() {
    if (null != rtcEngine) {
        rtcEngine.joinChannel(
                // 传入声网 RTC Token、频道名和 UID
                KeyCenter.RTC_TOKEN, KeyCenter.CHANNEL_ID, KeyCenter.RTC_UID,
                new ChannelMediaOptions() {{
                    // 发布音视频流
                    publishMicrophoneTrack = true;
                    publishCameraTrack = true;
                    // 订阅音视频流
                    autoSubscribeAudio = true;
                    autoSubscribeVideo = true;
                    // 设置用户角色为主播
                    clientRoleType = Constants.CLIENT_ROLE_BROADCASTER;
                }});
    }
}
 
private void addLocalAvatarView() {
    // 创建 SceneDisplayConfig 对象,用于设置场景视图的宽度和高度
    SceneDisplayConfig sceneDisplayConfig = new SceneDisplayConfig();
    sceneDisplayConfig.width = mLocalPreviewSurfaceView.getMeasuredWidth();
    sceneDisplayConfig.height = mLocalPreviewSurfaceView.getMeasuredHeight();
    MetaContext.getInstance().addSceneView(mLocalAvatarTextureView, sceneDisplayConfig);
}

// 在场景中添加一个新的视图,用于发布主播的 Avatar 形象
public void addSceneView(TextureView view, SceneDisplayConfig config) {
    if (null != metaScene && null != rtcEngine) {
        Log.i(TAG, "addRenderView view::" + view + ",config:" + config);
        // 设置视频编码属性,你可按需设置视频的分辨率、帧率、码率、视频方向和镜像模式等
        rtcEngine.setVideoEncoderConfiguration(new VideoEncoderConfiguration(
                new VideoEncoderConfiguration.VideoDimensions(330, 330),
                VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_30,
                STANDARD_BITRATE,
                VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_LANDSCAPE, VideoEncoderConfiguration.MIRROR_MODE_TYPE.MIRROR_MODE_DISABLED));
        // 将指定的 TextureView 对象添加到场景中,同时设置场景视图的配置信息
        metaScene.addSceneView(view, config);
    }
}
 
// 监听添加场景显示视图的回调事件
@Override
public void onAddSceneViewResult(TextureView view, int errorCode) {
    if (view.equals(mLocalAvatarTextureView)) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                MetaContext.getInstance().enableSceneVideo(mLocalAvatarTextureView, mEnableRemotePreviewAvatar);
            }
        });
    }
}

// 开启场景渲染画面捕获
// 默认为 false,即发送摄像头采集的视频画面,在融合场景中,建议设置为 true,把场景画面和主播的 Avatar 形象发布到频道
public void enableSceneVideo(TextureView view, boolean enable) {
    if (null != metaScene) {
        metaScene.enableSceneVideoCapture(view, enable);
    }
}

6 离开频道并释放资源

离开场景时,参考如下步骤:

  1. 调用 IMetaService 类的 removeSceneView 将 Avatar 直播画面从场景中移除。
  2. 通过 IMetaSceneEventHandler 类的 onRemoveSceneViewResult 回调监听移除场景显示视图的结果。
  3. 调用 RtcEngine 类的 leaveChannel 离开直播频道。
  4. 调用 IMetaService 类的 leaveScene 离开场景。
  5. 通过 IMetaSceneEventHandler 类的 onLeaveSceneResult 回调得知成功离开场景后,调用 release 释放 IMetaScene。
  6. 通过 IMetaSceneEventHandler 类的 onReleasedScene 回调监听 IMetaScene 是否释放成功。
  7. 依次调用 IMetaService 和 RtcEngine 类的 destroy 方法销毁 IMetaService 和 RtcEngine。
Java
// 移除场景显示视图,将 Avatar 直播画面从场景中移除
public void removeSceneView(TextureView view) {
    if (null != metaScene) {
        metaScene.removeSceneView(view);
    }
}
 
// 监听移除场景显示视图的回调事件
@Override
public void onRemoveSceneViewResult(TextureView view, int errorCode) {
    Log.d(TAG, String.format("onAddSceneViewResult %d", errorCode));
     
}

// 离开场景
private void leaveScene() {
    if (metaScene != null) {
        // 离开频道
        rtcEngine.leaveChannel();
        metaScene.leaveScene();
    }
}
 
// 监听离开场景的回调事件
@Override
public void onLeaveSceneResult(int errorCode) {
    Log.d(TAG, String.format("onLeaveSceneResult %d", errorCode));
    if (errorCode == 0) {
        // 释放 IMetaScene
        metaScene.release();
        metaScene = null;
    }
}

// 监听释放 IMetaScene 的回调事件
@Override
public void onReleasedScene(int status) {
    Log.d(TAG, String.format("[meta] onReleasedScene %d", status));
    destroy();
}
 
// 销毁 IMetaService 和 RtcEngine
public void destroy() {
    IMetaService.destroy();
    metaService = null;
    RtcEngine.destroy();
    rtcEngine = null;
}

开发注意事项

在使用 3D 场景的过程中,需要注意以下几点:

  • 为了保持场景的连续性和流畅性,通常不能销毁 Activity。为了避免销毁场景所在的 Activity,可以使用 Intent.FLAG_ACTIVITY_REORDER_TO_FRONT 标志将 Activity 置于后台。
  • 由于 Texture 的数据是在 GPU 中处理的,因此不能在运行时被销毁或重新创建,否则会影响应用程序的性能和稳定性。如果 Texture 尺寸大小发生变化,例如切换场景时需要切换横竖屏,你需要在setSurfaceTextureListener 的回调方法 onSurfaceTextureSizeChanged 中再次调用 createScene 和 enterScene 等方法,重新创建和进入场景。