三、步骤1:开通服务
语聊房场景通常需要依赖腾讯云 即时通信 IM 和 实时音视频 TRTC 两项付费 PaaS 服务构建。
1. 首先你需要登录 实时音视频 TRTC 控制台 创建应用,此时在 即时通信 IM 控制台 会同步自动创建一个与当前 TRTC 应用相同 SDKAppID 的 IM 体验版应用,二者账号与鉴权体系可复用。后续你可根据需要选择升级 TRTC 或 IM 应用版本,例如旗舰版可解锁更多增值功能服务。
注意:说明:
2. 创建应用完毕之后,你可以在应用管理 > 应用概览栏目看到该应用的基本信息,其中需要你保管好 SDKAppID、SDK 密钥便于后续使用,同时应避免密钥泄露造成流量盗刷。
四、步骤2:导入 SDK
你可以通过 pub add 的方式直接集成最新版本的腾讯云 IM SDK 和 TRTC SDK,或者在 pubspec.yaml 中手动写入的方式来集成。
·通过 flutter pub add 安装:
在终端窗口中输入如下命令(需要提前安装 Flutter 环境):
flutter pub add tencent_rtc_sdk #安装 TRTC SDKflutter pub add tencent_cloud_chat_sdk #安装 IM SDK
·在 pubspec.yaml 中手动写入:
# 在 pubspec.yaml 中找到 dependencies 添加以下依赖dependencies: # 可在 https://pub.dev/packages/tencent_rtc_sdk 上查看 trtc sdk 的最新版本并使用 tencent_rtc_sdk: "最新版本" # 可在 https://pub.dev/packages/tencent_cloud_chat_sdk 上查看 im sdk 的最新版本并使用 tencent_cloud_chat_sdk: "最新版本"
然后在终端窗口中输入 flutter pub get 进行安装。
flutter pub get
注意:说明:
五、步骤3:工程配置
iOSAndroidmacOS1. 需要在 /ios/Runner/Info.plist 的第一级<dict>目录下加入对相机和麦克风的权限申请:<key>NSCameraUsageDescription</key><string>授权摄像头权限才能正常视频通话</string><key>NSMicrophoneUsageDescription</key><string>授权麦克风权限才能正常语音通话</string>2. 接着在第一级<dict>目录下添加字段 io.flutter.embedded_views_preview,并设定值为 true。<key>io.flutter.embedded_views_preview</key><true/>1. 在 /android/app/src/main/AndroidManifest.xml 的<manifest>目录下添加如下权限:<uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /><uses-permission android:name="android.permission.RECORD_AUDIO" /><uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /><uses-permission android:name="android.permission.BLUETOOTH" /><uses-permission android:name="android.permission.CAMERA" /><uses-feature android:name="android.hardware.camera.autofocus" />2. 如果你需要编译运行在 Android 平台,你还需要进行如下配置:首先,需要在工程的android/app/build.gradle文件中对应位置添加: android { ….. packagingOptions { pickFirst 'lib/**/libliteavsdk.so' } buildTypes { release { …… minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } }}在工程的android/app目录下创建 proguard-rules.pro 文件,并在 proguard-rules.pro 文件中添加如下代码:-keep class com.tencent.** { *; }1. 打开 .xcworkspace 工程文件后,在 Xcode 的导航栏中点击左侧的 Project Navigator,点击 Runner 并确保在编辑区域选择正确的 TARGETS。2. 在 General 选项卡的 Frameworks, Libraries, and Embedded Content 部分添加 ScreenCaptureKit.framework。
注意:说明:
注意:如果你在接入过程中遇到问题,请参见 常见问题。
七、步骤1:生成鉴权凭证
UserSig 是腾讯云设计的一种安全保护签名,目的是为了阻止恶意攻击者盗用你的云服务使用权。腾讯云实时音视频(TRTC)、即时通信(IM)服务都采用了该套安全保护机制,TRTC 在进房时鉴权,IM 在登录时鉴权。
·调试跑通阶段:可以通过 客户端示例代码 和 控制台 两种方法计算生成 UserSig,仅用于调试测试。
·正式运行阶段:推荐安全等级更高的服务端计算 UserSig 方案,防止客户端被逆向破解泄露密钥。
具体实现流程如下:
1. 你的 App 在调用 SDK 的初始化函数之前,首先要向你的服务器请求 UserSig。
2. 你的服务器根据 SDKAppID 和 UserID 计算 UserSig。
3. 服务器将计算好的 UserSig 返回给你的 App。
4. 你的 App 将获得的 UserSig 通过特定 API 传递给 SDK。
5. SDK 将 SDKAppID + UserID + UserSig 提交给腾讯云服务器进行校验。
6. 腾讯云校验 UserSig,确认合法性。
7. 校验通过后,会向 IM SDK 提供即时通信服务、TRTC SDK 提供实时音视频服务。
九、时序图
1. IM SDK 初始化,添加 IM SDK 和信令的事件监听器。
// 从即时通信 IM 控制台获取应用 SDKAppID。int sdkAppID = 0;// 添加 V2TimSDKListener 的事件监听器V2TimSDKListener sdkListener = V2TimSDKListener( onConnecting: () { debugPrint("IM SDK 正在连接到腾讯云服务器"); }, onConnectSuccess: () { debugPrint("IM SDK 已经成功连接到腾讯云服务器"); },);// 初始化SDKV2TimValueCallback<bool> initSDKRes = await TencentImSDKPlugin.v2TIMManager.initSDK( sdkAppID: sdkAppID, // SDKAppID loglevel: LogLevelEnum.V2TIM_LOG_ALL, // 日志登记等级 listener: sdkListener, // 事件监听器);if (initSDKRes.code == 0) { //初始化成功}// 反初始化 IM SDKV2TimCallback uninitSDKRes = await TencentImSDKPlugin.v2TIMManager.unInitSDK();if(uninitSDKRes.code == 0) { //反初始化成功}// 添加 V2TimSignalingListener 信令的事件监听器V2TimSignalingListener signalListener = V2TimSignalingListener( onInviteeAccepted: (String inviteID, String invitee, String data) { // 远端接受通信请求回调逻辑 },);TencentImSDKPlugin.v2TIMManager.getSignalingManager().addSignalingListener(listener: signalListener);// 移除信令的事件监听器TencentImSDKPlugin.v2TIMManager.getSignalingManager().removeSignalingListener(listener: signalListener);
注意:说明:
2. TRTC SDK 创建实例与设置事件监听器。
// 创建 TRTC 实例TRTCCloud trtcCloud = await TRTCCloud.sharedInstance();// 来自 SDK 的各类事件通知(比如:错误码,警告码,音视频状态参数等)TRTCCloudListener listener = TRTCCloudListener( // 根据需要实现对应的回调 onError: (errCode, errMsg) { debugPrint("TRTC Error: $errCode, $errMsg"); }, onWarning: (int warningCode, String warningMsg){ debugPrint("TRTC Warning: $warningCode, $warningMsg"); });// 添加 TRTC 事件监听器trtcCloud.registerListener(listener);// 移除 TRTC 事件监听器trtcCloud.unRegisterListener(listener);// 销毁 TRTC 实例TRTCCloud.destroySharedInstance();
注意:说明:
注意:建议监听 SDK 事件通知,对一些常见错误进行日志打印和处理,详情请参见 错误码表。
十、步骤3:登录与登出
初始化 IM SDK 后,你需要调用 SDK 登录接口验证账号身份,获得账号的功能使用权限。因此在使用其他功能之前,请务必确保登录成功,否则可能导致功能异常或不可用。如你仅需使用 TRTC 音视频服务,可忽略此步骤。
十一、时序图
1. 登录。
// 登录:userID 可自定义,userSig 参见步骤1生成获取String userID = "your user id";String userSig = "userSig from your server";V2TimCallback loginRes = await TencentImSDKPlugin.v2TIMManager.login(userID: userID, userSig: userSig);if(loginRes.code == 0){ // 登录成功逻辑} else { // 登出失败逻辑 // 如果返回以下错误码,表示使用 UserSig 已过期,请你使用新签发的 UserSig 进行再次登录。 // 1. ERR_USER_SIG_EXPIRED(6206) // 2. ERR_SVR_ACCOUNT_USERSIG_EXPIRED(70001) // 注意:其他的错误码,请不要在这里调用登录接口,避免 IM SDK 登录进入死循环。 debugPrint("IM Login Error: ${loginRes.code}, ${loginRes.desc}");}
2. 登出。
V2TimCallback logoutRes = await TencentImSDKPlugin.v2TIMManager.logout();if(logoutRes.code == 0){ // 登出成功逻辑} else { // 登出失败逻辑 debugPrint("IM Login Error: ${logoutRes.code}, ${logoutRes.desc}");}
注意:说明:
注意:如果你的应用生命周期跟 IM SDK 生命周期一致,退出应用前可以不登出。若你只在进入特定界面后才使用 IM SDK,退出界面后不再使用,可以进行登出操作和对 IM SDK 进行反初始化。
十三、时序图
1. 创建房间。主播(房主)开播时需要创建房间,这里的“房间”概念对应 IM 中的“群组”。本例仅展示客户端创建 IM 群组的方式,实际也可在 服务端创建群组。
V2TimValueCallback<String> createRes = await TencentImSDKPlugin.v2TIMManager.getGroupManager().createGroup( groupID: groupID, groupType: "AVChatRoom", groupName: groupName);if(createRes.code == 0) { // 如果没有自定义群组 ID,则创建成功后回调返回系统会自动分配的 groupID String groupID = createRes.data!;} else { // 创建群组失败}TencentImSDKPlugin.v2TIMManager.addGroupListener( listener: V2TimGroupListener( onGroupCreated: (String groupID) { // 群创建回调,groupID 为新创建群组的 ID }, ));
2. 加入房间。
·加入 IM 群组。
V2TimCallback joinRes = await TencentImSDKPlugin.v2TIMManager.joinGroup(groupID: groupID, message: message);if(joinRes.code == 0) { // 加入群组成功} else { // 加入群组失败}TencentImSDKPlugin.v2TIMManager.addGroupListener( listener: V2TimGroupListener( onMemberEnter: (String groupID, List<V2TimGroupMemberInfo> memberList) { // 有人加入群组 }, ));
·加入 TRTC 房间。
TRTCParams params = TRTCParams( sdkAppId: SDKAPPID, // TRTC应用标识, 在控制台获取 userSig: USERSIG, // TRTC鉴权凭证, 在服务端生成 strRoomId: roomId, // 以字符串房间号为例,建议和 IM 群组号保持一致 userId: userId, // 用户名, 建议和IM保持同步 role: TRTCRoleType.audience // 语聊互动场景进房需指定用户角色);// 场景为语音聊天房trtcCloud.enterRoom(params, TRTCAppScene.voiceChatRoom);trtcCloud.registerListener( TRTCCloudListener( onEnterRoom: (int result) { if (result > 0 ) { // 进房成功,result 代表加入房间所消耗的时间(毫秒) } else { // 进房失败,result 代表错误码 } } ));
3. 退出房间。
·退出 IM 群组。
V2TimCallback quitRes = await TencentImSDKPlugin.v2TIMManager.quitGroup(groupID: groupID);if(quitRes.code == 0) { // 退出群组成功} else { // 退出群组失败}TencentImSDKPlugin.v2TIMManager.addGroupListener( listener: V2TimGroupListener( onQuitFromGroup: (String groupID) { // 退群者会收到离开群组回调 }, onMemberLeave: (String groupID, V2TimGroupMemberInfo member) { // 群组内其他成员会收到群成员离开群组回调 }, ));
注意:直播群(AVChatRoom)中,群主是不可以退群的,群主只能调用 dismissGroup 解散群组。
·退出 TRTC 房间。
void _exitRoom() { trtcCloud.stopLocalAudio(); trtcCloud.exitRoom();}TRTCCloudListener( onExitRoom: (int reason) { if (reason == 0) { debugPrint("主动调用 exitRoom 退出房间"); } else if (reason == 1) { debugPrint("被服务器踢出当前房间"); } else if (reason == 2) { debugPrint("当前房间整个被解散"); } });
4. 解散房间。
·解散 IM 群组。本例仅展示客户端解散 IM 群组的方式,实际也可在 服务端解散群组。
V2TimCallback dismissRes = await TencentImSDKPlugin.v2TIMManager.dismissGroup(groupID: groupID);if(dismissRes.code == 0) { // 解散群组成功} else { // 解散群组失败}TencentImSDKPlugin.v2TIMManager.addGroupListener( listener: V2TimGroupListener( onGroupDismissed: (String groupID, V2TimGroupMemberInfo opUser,) { // 群被解散回调 } ));
·解散 TRTC 房间。
·服务端解散:TRTC 提供了 服务端解散房间 API DismissRoom(区分数字房间 ID 和字符串房间 ID),你可以调用此接口把房间所有用户从房间移出,并解散房间。
·客户端解散:通过各个客户端的退出房间 exitRoom 接口,将房间内的所有主播和听众完成退房,退房后,根据 TRTC 房间生命周期规则,房间将会自动解散,详情请参见 退出房间。
注意:建议当你的一次直播任务结束后,可以调用解散房间 API 确保房间解散,防止听众意外进房导致产生非期望的费用。
十五、时序图
首先,我们可以创建一个类用于保存麦位信息。
class SeatInfo{ static const int STATUS_UNUSED = 0; static const int STATUS_USED = 1; static const int STATUS_LOCKED = 2; // 座位状态,对应三种状态 int status; // 座位是否禁言 bool mute; // 座位占用时,存储用户信息 String? userId; SeatInfo({ required this.status, required this.mute, this.userId, }); @override String toString() { return 'TXSeatInfo{status=$status, mute=$mute, userId=$userId}'; }}
1. 主动上麦。主动上麦是指麦下听众向房主或管理员发送上麦申请,待接收到同意信令后上麦。如为自由上麦模式,则可忽略信令请求部分。
·听众发送上麦请求。
// 听众发送上麦请求,userId 为主播 ID,data 可传入标识信令的 jsonV2TimValueCallback<String> inviteRes = await TencentImSDKPlugin.v2TIMManager.getSignalingManager().invite( invitee: userId, data: data, timeout: timeout, onlineUserOnly: true, offlinePushInfo: null);if(inviteRes.code == 0){ // 发送请求上麦请求成功} else { // 发送请求上麦请求失败}// 主播收到上麦请求, inviteID 为该条请求 ID,inviter 为请求者 IDTencentImSDKPlugin.v2TIMManager.getSignalingManager().addSignalingListener( listener: V2TimSignalingListener( onReceiveNewInvitation: (String inviteID, String inviter, String groupID, List<String> inviteeList, String data) { debugPrint("received invitation: $inviteID from $inviter"); } ));
·主播处理上麦请求。
// 同意上麦请求V2TimCallback acceptRes = await TencentImSDKPlugin.v2TIMManager.getSignalingManager().accept(inviteID: inviteID, data: data);if(acceptRes.code == 0) { // 同意上麦请求成功} else { // 同意上麦请求失败}// 拒绝上麦请求V2TimCallback rejectRes = await TencentImSDKPlugin.v2TIMManager.getSignalingManager().reject(inviteID: inviteID, data: data);if(rejectRes.code == 0) { // 拒绝上麦请求成功} else { // 拒绝上麦请求失败}
·听众上麦。如果主播同意听众的上麦请求,听众可以通过修改群属性的方式添加麦位信息,其他用户会收到群属性变更回调,更新本地麦位信息。
// 本地保存的全量麦位信息列表List<SeatInfo> _SeatInfoList;// 同意上麦请求的回调TencentImSDKPlugin.v2TIMManager.getSignalingManager().addSignalingListener( listener: V2TimSignalingListener( onInviteeAccepted: (String inviteID, String invitee, String data) { debugPrint("received accept invitation: $inviteID from $invitee"); _takeSeat(seatIndex); } ));void _takeSeat(int seatIndex) async { // 创建麦位信息实例,存储修改后的麦位信息 SeatInfo localInfo = _SeatInfoList[seatIndex]; SeatInfo seatInfo = new SeatInfo( status: SeatInfo.STATUS_USED, mute: localInfo.mute, userId: _UserId ); // 将麦位信息对象序列化为 JSON 格式 String jsonStr = json.encode(seatInfo); Map<String, String> map = { "seat$seatIndex": jsonStr }; // 设置群属性,已有该群属性则更新其 value 值,没有该群属性则添加该属性 V2TimCallback groupAttrRes = await TencentImSDKPlugin.v2TIMManager.getGroupManager().setGroupAttributes(groupID: groupID, attributes: map); if(groupAttrRes.code == 0) { // 修改群属性成功,切换 TRTC 角色并开始推流 trtcCloud.switchRole(TRTCRoleType.anchor); trtcCloud.startLocalAudio(TRTCAudioQuality.defaultMode); } else { // 修改群属性失败,上麦失败 }}
2. 抱人上麦。主播抱人上麦(无需听众同意),直接修改群属性保存的麦位信息,对应听众收到群属性变更回调后匹配 userId 成功即可切换 TRTC 角色并开始推流。如为邀请上麦模式,可参照主动上麦的实现逻辑,只需调换信令的发送方与接收方即可。
// 本地保存的全量麦位信息列表List<SeatInfo> _SeatInfoList;// 主播端调用该接口修改群属性保存的麦位信息void _pickSeat(String userId, int seatIndex) async { SeatInfo localInfo = _SeatInfoList[seatIndex]; SeatInfo seatInfo = new SeatInfo( status: SeatInfo.STATUS_USED, mute: localInfo.mute, userId: userId ); // 将麦位信息对象序列化为 JSON 格式 String jsonStr = json.encode(seatInfo); Map<String, String> map = { "seat$seatIndex": jsonStr }; // 设置群属性,已有该群属性则更新其 value 值,没有该群属性则添加该属性 V2TimCallback groupAttrRes = await TencentImSDKPlugin.v2TIMManager.getGroupManager().setGroupAttributes(groupID: groupID, attributes: map); if(groupAttrRes.code == 0) { // 修改群属性成功,触发 onGroupAttributeChanged 回调 trtcCloud.switchRole(TRTCRoleType.anchor); trtcCloud.startLocalAudio(TRTCAudioQuality.defaultMode); } else { // 修改群属性失败,抱麦失败 }}// 听众端收到群属性变更回调,匹配自身信息成功后开始推流TencentImSDKPlugin.v2TIMManager.addGroupListener( listener: V2TimGroupListener( onGroupAttributeChanged: (String groupID, Map<String, String> groupAttributeMap) { // 上一次本地保存的全量麦位信息列表 final List<SeatInfo> oldSeatInfoList = _SeatInfoList; // 最新从 groupAttributeMap 中解析的全量麦位信息列表 final List<SeatInfo> newSeatInfoList = getSeatListFromAttr(groupAttributeMap, seatSize); // 遍历全量麦位信息列表,对比新旧麦位信息 for (int i = 0; i < seatSize; i++) { SeatInfo oldInfo = oldSeatInfoList[i]; SeatInfo newInfo = newSeatInfoList[i]; if (oldInfo.status != newInfo.status && newInfo.status == SeatInfo.STATUS_USED) { if (newInfo.userId == _mUserId) { // 匹配自身信息成功,切换 TRTC 角色并开始推流 trtcCloud.switchRole(TRTCRoleType.anchor); trtcCloud.startLocalAudio(TRTCAudioQuality.defaultMode); } else { // 更新本地麦位列表,渲染本地麦位视图 } } } } ));
3. 主动下麦。连麦听众可以通过修改群属性的方式重置麦位信息,其他用户会收到群属性变更回调,更新本地麦位信息。
// 本地保存的全量麦位信息列表List<SeatInfo> _SeatInfoList;void _leaveSeat(int seatIndex) async { // 创建麦位信息实例,存储修改后的麦位信息 SeatInfo localInfo = _SeatInfoList[seatIndex]; SeatInfo seatInfo = new SeatInfo( status: SeatInfo.STATUS_UNUSED, mute: localInfo.mute, userId: "" ); // 将麦位信息对象序列化为 JSON 格式 String jsonStr = json.encode(seatInfo); Map<String, String> map = { "seat$seatIndex": jsonStr }; // 设置群属性,已有该群属性则更新其 value 值,没有该群属性则添加该属性 V2TimCallback groupAttrRes = await TencentImSDKPlugin.v2TIMManager.getGroupManager().setGroupAttributes(groupID: groupID, attributes: map); if(groupAttrRes.code == 0) { // 修改群属性成功,切换 TRTC 角色并停止推流 trtcCloud.switchRole(TRTCRoleType.audience); trtcCloud.stopLocalAudio(); } else { // 修改群属性失败,抱麦失败 }}
4. 踢人下麦。主播踢人下麦,直接修改群属性保存的麦位信息,对应连麦听众收到群属性变更回调后匹配 userId 成功即可切换 TRTC 角色并停止推流。
// 本地保存的全量麦位信息列表List<SeatInfo> _SeatInfoList;// 主播端调用该接口修改群属性保存的麦位信息void _kickSeat(int seatIndex) async { // 创建麦位信息实例,存储修改后的麦位信息 SeatInfo localInfo = _SeatInfoList[seatIndex]; SeatInfo seatInfo = new SeatInfo( status: SeatInfo.STATUS_UNUSED, mute: localInfo.mute, userId: "" ); // 将麦位信息对象序列化为 JSON 格式 String jsonStr = json.encode(seatInfo); Map<String, String> map = { "seat$seatIndex": jsonStr }; // 设置群属性,已有该群属性则更新其 value 值,没有该群属性则添加该属性 V2TimCallback groupAttrRes = await TencentImSDKPlugin.v2TIMManager.getGroupManager().setGroupAttributes(groupID: groupID, attributes: map); if(groupAttrRes.code == 0) { // 修改群属性成功,触发 onGroupAttributeChanged 回调 } else { // 修改群属性失败,踢麦失败 }}// 连麦听众端收到群属性变更回调,匹配自身信息成功后停止推流TencentImSDKPlugin.v2TIMManager.addGroupListener( listener: V2TimGroupListener( onGroupAttributeChanged: (String groupID, Map<String, String> groupAttributeMap) { // 上一次本地保存的全量麦位信息列表 final List<SeatInfo> oldSeatInfoList = _SeatInfoList; // 最新从 groupAttributeMap 中解析的全量麦位信息列表 final List<SeatInfo> newSeatInfoList = getSeatListFromAttr(groupAttributeMap, seatSize); // 遍历全量麦位信息列表,对比新旧麦位信息 for (int i = 0; i < seatSize; i++) { SeatInfo oldInfo = oldSeatInfoList[i]; SeatInfo newInfo = newSeatInfoList[i]; if (oldInfo.status != newInfo.status && newInfo.status == SeatInfo.STATUS_UNUSED) { if (oldInfo.userId == _mUserId) { // 匹配自身信息成功,切换 TRTC 角色并开始推流 trtcCloud.switchRole(TRTCRoleType.audience); trtcCloud.stopLocalAudio(); } else { // 更新本地麦位列表,渲染本地麦位视图 } } } } ));
5. 麦位禁音。主播禁音/解禁某个麦位,直接修改群属性保存的麦位信息,对应连麦听众收到群属性变更回调后匹配 userId 成功即可暂停/恢复本地推流。
// 本地保存的全量麦位信息列表List<SeatInfo> _SeatInfoList;// 主播端调用该接口修改群属性保存的麦位信息void _muteSeat(int seatIndex, bool mute) async { // 创建麦位信息实例,存储修改后的麦位信息 SeatInfo localInfo = _SeatInfoList[seatIndex]; SeatInfo seatInfo = new SeatInfo( status: localInfo.status, mute: mute, userId: localInfo.userId ); // 将麦位信息对象序列化为 JSON 格式 String jsonStr = json.encode(seatInfo); Map<String, String> map = { "seat$seatIndex": jsonStr }; // 设置群属性,已有该群属性则更新其 value 值,没有该群属性则添加该属性 V2TimCallback groupAttrRes = await TencentImSDKPlugin.v2TIMManager.getGroupManager().setGroupAttributes(groupID: groupID, attributes: map); if(groupAttrRes.code == 0) { // 修改群属性成功,触发 onGroupAttributeChanged 回调 } else { // 修改群属性失败,禁麦失败 }}// 连麦听众端收到群属性变更回调,匹配自身信息成功后暂停/恢复推流TencentImSDKPlugin.v2TIMManager.addGroupListener( listener: V2TimGroupListener( onGroupAttributeChanged: (String groupID, Map<String, String> groupAttributeMap) { // 上一次本地保存的全量麦位信息列表 final List<SeatInfo> oldSeatInfoList = _SeatInfoList; // 最新从 groupAttributeMap 中解析的全量麦位信息列表 final List<SeatInfo> newSeatInfoList = getSeatListFromAttr(groupAttributeMap, seatSize); // 遍历全量麦位信息列表,对比新旧麦位信息 for (int i = 0; i < seatSize; i++) { SeatInfo oldInfo = oldSeatInfoList[i]; SeatInfo newInfo = newSeatInfoList[i]; if (oldInfo.mute != newInfo.mute) { if (oldInfo.userId == _mUserId) { // 匹配自身信息成功,暂停/恢复本地推流 trtcCloud.muteLocalAudio(newInfo.mute); } else { // 更新本地麦位列表,渲染本地麦位视图 } } } } ));
6. 麦位锁定。主播锁定/解锁某个麦位,直接修改群属性保存的麦位信息,听众收到群属性变更回调后更新对应麦位视图。
// 本地保存的全量麦位信息列表List<SeatInfo> _SeatInfoList;// 主播端调用该接口修改群属性保存的麦位信息void _lockSeat(int seatIndex, bool isLock) async { // 创建麦位信息实例,存储修改后的麦位信息 SeatInfo localInfo = _SeatInfoList[seatIndex]; SeatInfo seatInfo = new SeatInfo( status: isLock ? SeatInfo.STATUS_LOCKED : SeatInfo.STATUS_UNUSED, mute: mute, userId: localInfo.userId ); // 将麦位信息对象序列化为 JSON 格式 String jsonStr = json.encode(seatInfo); Map<String, String> map = { "seat$seatIndex": jsonStr }; // 设置群属性,已有该群属性则更新其 value 值,没有该群属性则添加该属性 V2TimCallback groupAttrRes = await TencentImSDKPlugin.v2TIMManager.getGroupManager().setGroupAttributes(groupID: groupID, attributes: map); if(groupAttrRes.code == 0) { // 修改群属性成功,触发 onGroupAttributeChanged 回调 } else { // 修改群属性失败,锁麦失败 }}// 听众端收到群属性变更回调,更新对应麦位视图TencentImSDKPlugin.v2TIMManager.addGroupListener( listener: V2TimGroupListener( onGroupAttributeChanged: (String groupID, Map<String, String> groupAttributeMap) { // 上一次本地保存的全量麦位信息列表 final List<SeatInfo> oldSeatInfoList = _SeatInfoList; // 最新从 groupAttributeMap 中解析的全量麦位信息列表 final List<SeatInfo> newSeatInfoList = getSeatListFromAttr(groupAttributeMap, seatSize); // 遍历全量麦位信息列表,对比新旧麦位信息 for (int i = 0; i < seatSize; i++) { SeatInfo oldInfo = oldSeatInfoList[i]; SeatInfo newInfo = newSeatInfoList[i]; if (oldInfo.mute != newInfo.mute) { if (oldInfo.status == SeatInfo.STATUS_LOCKED && newInfo.status == SeatInfo.STATUS_UNUSED) { // 解锁麦位 } else if (oldInfo.status != newInfo.status && newInfo.status == SeatInfo.STATUS_LOCKED) { // 锁定麦位 } } } } ));
7. 麦位移动。麦上主播移动麦位,需要分别修改群属性保存的源和目标麦位信息,听众收到群属性变更回调后更新对应麦位视图。
// 本地保存的全量麦位信息列表List<SeatInfo> _SeatInfoList;// 麦上主播调用该接口修改群属性保存的麦位信息void _moveSeat(int dstIndex) async { // 根据 userId 获取源麦位编号 int srcIndex = -1; for (int i = 0; i < _SeatInfoList.size(); i++) { SeatInfo seatInfo = _SeatInfoList[i]; if (seatInfo != null && _UserId == seatInfo.userId) { srcIndex = i; break; } } // 根据麦位编号获取对应麦位信息 SeatInfo srcSeatInfo = _SeatInfoList[srcIndex]; SeatInfo dstSeatInfo = _SeatInfoList[dstIndex]; // 创建麦位信息实例,存储修改后的源麦位信息 SeatInfo srcChangeInfo = new SeatInfo( status: SeatInfo.STATUS_UNUSED, mute: srcSeatInfo.mute, userId: "" ); // 创建麦位信息实例,存储修改后的目标麦位信息 SeatInfo dstChangeInfo = new SeatInfo( status: SeatInfo.STATUS_USED, mute: srcSeatInfo.mute, userId: _UserId ); // 将麦位信息对象序列化为 JSON 格式 String srcJson = json.encode(srcChangeInfo); String dstJson = json.encode(dstChangeInfo); Map<String, String> map = { "seat$srcIndex": srcJson, "seat$dstIndex": dstJson, }; // 设置群属性,已有该群属性则更新其 value 值,没有该群属性则添加该属性 V2TimCallback groupAttrRes = await TencentImSDKPlugin.v2TIMManager.getGroupManager().setGroupAttributes(groupID: groupID, attributes: map); if(groupAttrRes.code == 0) { // 修改群属性成功,移麦成功 } else { // 修改群属性失败,移麦失败 }}
十七、时序图
1. 订阅模式。
TRTC SDK 默认为自动订阅音频流逻辑,用户进房会自动开始播放远端用户的声音。如有手动订阅音频流的需求,需要额外调用 muteRemoteAudio(userId, mute) 来订阅和播放远端用户音频流。
// 自动订阅模式(默认)trtcCloud.setDefaultStreamRecvMode(true, true);// 手动订阅模式(自定义)trtcCloud.setDefaultStreamRecvMode(false, false);
注意:设置订阅模式 setDefaultStreamRecvMode 必须在进房 enterRoom 之前调用才会生效。
2. 采集与发布。
// 开启本地音频的采集和发布trtcCloud.startLocalAudio(TRTCAudioQuality.defaultMode);// 停止本地音频的采集和发布trtcCloud.stopLocalAudio();
注意:说明:
注意:startLocalAudio 会申请麦克风使用权限,stopLocalAudio 会释放麦克风使用权限。
3. 闭麦与开麦。
// 暂停发布本地音频流(闭麦)trtcCloud.muteLocalAudio(true);// 恢复发布本地音频流(开麦)trtcCloud.muteLocalAudio(false);// 暂停订阅和播放某远端用户的音频流trtcCloud.muteRemoteAudio(userId, true);// 恢复订阅和播放某远端用户的音频流trtcCloud.muteRemoteAudio(userId, false);// 暂停订阅和播放所有远端用户的音频流trtcCloud.muteAllRemoteAudio(true);// 恢复订阅和播放所有远端用户的音频流trtcCloud.muteAllRemoteAudio(false);
注意:说明:
注意:相比之下,muteLocalAudio 只需要在软件层面对数据流进行暂停或者放行即可,因此效率更高更平滑,也更适合需要频繁开闭麦的场景。
4. 音质及音频路由。
·音质设置。
// 本地音频采集和发布时设置音质trtcCloud.startLocalAudio(TRTCAudioQuality.defaultMode);
注意:说明:
注意:TRTC 预设音质共分为三档(Speech/Default/Music)分别对应不同的音频参数,详情请参见 音频音质。
·音频路由设置。
移动端设备上通常有扬声器和听筒两个播放位置,如需强制指定音频路由可以使用如下接口。
// 设置扬声器播放音频trtcCloud.getDeviceManager().setAudioRoute(TXAudioRoute.speakerPhone);// 设置听筒播放音频trtcCloud.getDeviceManager().setAudioRoute(TXAudioRoute.earpiece);
十九、弹幕消息互动
语聊直播间通常会有文本形式的弹幕消息互动,这里可以通过 IM 的发送及接收群聊普通文本消息来实现。
// 创建文本消息V2TimValueCallback<V2TimMsgCreateInfoResult> createMsgRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().createTextMessage( // 要发送的文本信息 text: text );// 发送公屏弹幕消息V2TimValueCallback<V2TimMessage> sendRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage( message: createMsgRes.data?.messageInfo, // 群发弹幕消息,不填写 receiver,只填写相应 groupID receiver: '', groupID: groupID, // 仅给在线用户推送弹幕消息 onlineUserOnly: true, // 普通优先级,用于普通消息 priority: MessagePriorityEnum.V2TIM_PRIORITY_NORMAL);if (sendRes.code == 0) { // 发送弹幕消息成功} else { // 发送弹幕消息失败}// 接收公屏弹幕消息TencentImSDKPlugin.v2TIMManager.getMessageManager().addAdvancedMsgListener( listener: V2TimAdvancedMsgListener( onRecvNewMessage: (V2TimMessage msg) { debugPrint("$msg.nickName: $msg.textElem.text"); } ));
20、音量大小回调
TRTC 可以按照固定频率回调麦上主播的音量大小,通常用于展示音波音浪,提示正在发言的主播。
TRTCAudioVolumeEvaluateParams evaParams = new TRTCAudioVolumeEvaluateParams( enablePitchCalculation: true, // 启用音高计算 enableSpectrumCalculation: true, // 启用频谱计算 enableVadDetection: true, // 启用语音活动检测 interval: 300 // 回调间隔(ms));// 启用音量大小回调,建议在进房成功后即开启trtcCloud.enableAudioVolumeEvaluation(true, evaParams);trtcCloud.registerListener( TRTCCloudListener( onUserVoiceVolume: (List<TRTCVolumeInfo> userVolumes, int totalVolume) { // userVolumes 用于承载所有正在说话的用户的音量大小,包括本地用户和远端推流用户 // totalVolume 用于反馈所有远端推流用户的总音量大小 // 根据音量大小在 UI 上做出相应的音浪展示 } ));
21、音乐及音效播放
播放背景音乐及音效是语聊房场景中的高频需求,下面将对常用的背景音乐相关接口的使用及注意事项进行说明。
1. 开始/停止/暂停/恢复播放。
AudioMusicParam musicParams = new AudioMusicParam( id: musicID, // 音乐 ID path: musicPath, // 音效文件的完整路径或 URL 地址 loopCount: loopCount, // 音乐循环播放的次数 publish: true, // 是否将音乐发布到远端(否则仅本地播放) isShortFile: false, // 播放的是否为短音效文件);// 开始播放背景音乐trtcCloud.getAudioEffectManager().startPlayMusic(musicParams);// 停止播放背景音乐trtcCloud.getAudioEffectManager().stopPlayMusic(musicID);// 暂停播放背景音乐trtcCloud.getAudioEffectManager().pausePlayMusic(musicID);// 恢复播放背景音乐trtcCloud.getAudioEffectManager().resumePlayMusic(musicID);
2. 调节音乐及人声音量占比。
// 设置某一首背景音乐的本地播放音量的大小trtcCloud.getAudioEffectManager().setMusicPlayoutVolume(musicID, volume);// 设置某一首背景音乐的远端播放音量的大小trtcCloud.getAudioEffectManager().setMusicPublishVolume(musicID, volume);// 设置所有背景音乐的本地音量和远端音量的大小trtcCloud.getAudioEffectManager().setAllMusicVolume(volume);// 设置人声采集音量的大小trtcCloud.getAudioEffectManager().setVoiceCaptureVolume(volume);
3. 设置音乐播放的事件回调。
trtcCloud.getAudioEffectManager().setMusicObserver(musicID, TXMusicPlayObserver( // 背景音乐开始播放 onStart: (int id, int errorCode) { // 0 开始播放成功 // -4001 打开文件失败 // -4005 非法路径导致打开文件失败 // -4006 非法URL导致打开文件失败 // -4007 无音频流导致打开文件失败 // -4008 格式不支持导致打开文件失败 if (errorCode < 0) { // 播放失败后重新播放前需要先停止播放当前音乐 trtcCloud.getAudioEffectManager().stopPlayMusic(id); } }, // 背景音乐的播放速度 onPlayProgress: (int id, int curPtsMSm, int durationMS) { // curPtsMSm 当前播放时间点(毫秒) // durationMS 总持续时长(毫秒) }, // 背景音乐已经播放完毕 onComplete: (int id, int errorCode) { // 0 播放结束 // -4002 解码失败 } ));
4. 循环播放背景音乐及音效。
·方案一:使用 AudioMusicParam 中的 loopCount 参数设置循环播放次数。
取值范围为0 – 任意正整数,默认值:0。0 表示播放音乐一次;1 表示播放音乐两次;以此类推。
void _startPlayMusic(int musicID, String musicPath, int loopCount) { AudioMusicParam musicParams = new AudioMusicParam( id: musicID, path: musicPath, // 设定循环播放次数,负数表示无限循环用一个大整数表示 loopCount: loopCount < 0 ? bigInt : loopCount, // 是否将音乐发布到远端 publish: true, // 播放的是否为短音效文件 isShortFile: false, ); trtcCloud.getAudioEffectManager().startPlayMusic(musicParams);}
注意:方案一每次循环播放完毕并不会触发 onComplete 回调,只有等设置的循环次数全部播放完毕才会触发该回调。
·方案二:通过“背景音乐已经播放完毕”的事件回调 onComplete 来实现循环播放,通常用于列表循环或单曲循环。
// 标识是否循环播放的成员变量private boolean loopPlay;private void startPlayMusic(int id, String path) { TXAudioEffectManager.AudioMusicParam param = new TXAudioEffectManager.AudioMusicParam(id, path); mTXAudioEffectManager.setMusicObserver(id, new MusicPlayObserver(id, path)); mTXAudioEffectManager.startPlayMusic(param);}private class MusicPlayObserver implements TXAudioEffectManager.TXMusicPlayObserver { private final int mId; private final String mPath; public MusicPlayObserver(int id, String path) { mId = id; mPath = path; } @Override public void onStart(int i, int i1) { } @Override public void onPlayProgress(int i, long l, long l1) { } @Override public void onComplete(int i, int i1) { mTXAudioEffectManager.stopPlayMusic(i); if (i1 >= 0 && loopPlay) { // 这里可替换循环列表音乐的 ID、Path startPlayMusic(mId, mPath); } }}
22、混流转推及回推
1. 混流转推直播 CDN。
void _startPublishMediaToCDN(String streamName) { // 推流地址的过期时间,默认一天 final int txTime = ((DateTime.now().millisecondsSinceEpoch) / 1000 + (24* 60* 60)) as int; // LIVE_URL_KEY 鉴权密钥以及 getSafeUrl 方法请在云直播控制台推流地址配置页面获取 String secretParam = UrlHelper.getSafeUrl(LIVE_URL_KEY, streamName, txTime); // 媒体流发布的目标地址 TRTCPublishTarget target = new TRTCPublishTarget(); // 混流后发布到 CDN target.mode = TRTCPublishMode.mixStreamToCdn; TRTCPublishCdnUrl cdnUrl = new TRTCPublishCdnUrl(); // 推流地址必须带参数,否则推流不成功 cdnUrl.rtmpUrl = "rtmp://" + PUSH_DOMAIN + "/live/" + streamName + "?" + secretParam; // 腾讯云直播推流地址为 true,第三方为 false cdnUrl.isInternalLine = true; // 可以添加多个 CDN 推流地址 target.cdnUrlList.add(cdnUrl); // 设置转码后的音频流的编码参数(可自定义) TRTCStreamEncoderParam trtcStreamEncoderParam = new TRTCStreamEncoderParam(); trtcStreamEncoderParam.audioEncodedChannelNum = 1; trtcStreamEncoderParam.audioEncodedKbps = 50; trtcStreamEncoderParam.audioEncodedCodecType = 0; trtcStreamEncoderParam.audioEncodedSampleRate = 48000; // 设置转码后的视频流的编码参数(若需混入黑帧则必填,纯音频混流可忽略) trtcStreamEncoderParam.videoEncodedFPS = 15; trtcStreamEncoderParam.videoEncodedGOP = 3; trtcStreamEncoderParam.videoEncodedKbps = 30; trtcStreamEncoderParam.videoEncodedWidth = 64; trtcStreamEncoderParam.videoEncodedHeight = 64; // 媒体流转码配置参数 TRTCStreamMixingConfig trtcStreamMixingConfig = new TRTCStreamMixingConfig(); // 默认情况下填空值即可,代表会混合房间中的所有音频 trtcStreamMixingConfig.audioMixUserList = []; // 若需混入黑帧则必须携带 TRTCVideoLayout 参数(纯音频混流可忽略) TRTCVideoLayout videoLayout = new TRTCVideoLayout(); trtcStreamMixingConfig.videoLayoutList.add(videoLayout); // 开始混流转推 trtcCloud.startPublishMediaStream(target, trtcStreamEncoderParam, trtcStreamMixingConfig);}
注意:说明:
注意:推流地址除以上手动拼接的方法,还可以在云直播的 地址生成器 中一键生成。
2. 混流回推 TRTC 房间。
void _startPublishMediaToRoom(String roomId, String userId) { // 创建 TRTCPublishTarget 对象 TRTCPublishTarget target = new TRTCPublishTarget(); // 混流后回推到房间 target.mode = TRTCPublishMode.mixStreamToRoom; target.mixStreamIdentity.strRoomId = roomId; // 混流机器人的 userid,不能和房间内其他用户的 userid 重复 target.mixStreamIdentity.userId = userId + MIX_ROBOT; // 设置转码后的音频流的编码参数(可自定义) TRTCStreamEncoderParam trtcStreamEncoderParam = new TRTCStreamEncoderParam(); trtcStreamEncoderParam.audioEncodedChannelNum = 1; trtcStreamEncoderParam.audioEncodedKbps = 50; trtcStreamEncoderParam.audioEncodedCodecType = 0; trtcStreamEncoderParam.audioEncodedSampleRate = 48000; // 设置转码后的视频流的编码参数(纯音频混流可忽略) trtcStreamEncoderParam.videoEncodedFPS = 15; trtcStreamEncoderParam.videoEncodedGOP = 3; trtcStreamEncoderParam.videoEncodedKbps = 30; trtcStreamEncoderParam.videoEncodedWidth = 64; trtcStreamEncoderParam.videoEncodedHeight = 64; // 设置音频混流参数 TRTCStreamMixingConfig trtcStreamMixingConfig = new TRTCStreamMixingConfig(); // 默认情况下填空值即可,代表会混合房间中的所有音频 trtcStreamMixingConfig.audioMixUserList = []; // 配置视频混流模板(纯音频混流可忽略) TRTCVideoLayout videoLayout = new TRTCVideoLayout(); trtcStreamMixingConfig.videoLayoutList.add(videoLayout); // 开始混流回推 trtcCloud.startPublishMediaStream(target, trtcStreamEncoderParam, trtcStreamMixingConfig);}
3. 事件回调及更新停止任务。
·任务结果事件回调。
trtcCloud.registerListener( TRTCCloudListener( onStartPublishMediaStream: (String taskId, int errCode, String errMsg, String extraInfo) { // taskId: 当请求成功时,TRTC 后台会在回调中提供给你这项任务的 taskId,后续你可以通过该 taskId 结合 updatePublishMediaStream 和 stopPublishMediaStream 进行更新和停止 // errCode: 回调结果,0 表示成功,其余值表示失败 }, onUpdatePublishMediaStream: (String taskId, int errCode, String errMsg, String extraInfo) { // 你调用媒体流发布接口 (updatePublishMediaStream) 时传入的 taskId,会通过此回调再带回给你,用于标识该回调属于哪一次更新请求 // errCode: 回调结果,0 表示成功,其余值表示失败 }, onStopPublishMediaStream: (String taskId, int errCode, String errMsg, String extraInfo) { // 你调用停止发布媒体流 (stopPublishMediaStream) 时传入的 taskId,会通过此回调再带回给你,用于标识该回调属于哪一次停止请求 // errCode: 回调结果,0 表示成功,其余值表示失败 } ));
·更新发布媒体流。
该接口会向 TRTC 服务器发送指令,更新通过 startPublishMediaStream 启动的媒体流。
// taskId: 通过 onStartPublishMediaStream 回调的任务 ID// target: 例如增删发布的 CDN URL// trtcStreamEncoderParam: 建议保持媒体流编码输出参数一致,避免播放侧断流// trtcStreamMixingConfig: 更新参与混流转码的用户列表,例如跨房 PKtrtcCloud.updatePublishMediaStream(taskId, target, trtcStreamEncoderParam, trtcStreamMixingConfig);
注意:同一个任务不支持纯音频、音视频、纯视频之间的切换。
·停止发布媒体流。
该接口会向 TRTC 服务器发送指令,停止通过 startPublishMediaStream 启动的媒体流。
// taskId: 通过 onStartPublishMediaStream 回调的任务 IDtrtcCloud.stopPublishMediaStream(taskId);
注意:若 taskId 填空字符串,将会停止该用户所有通过 startPublishMediaStream 启动的媒体流,如果你只启动了一个媒体流或者想停止所有通过你启动的媒体流,推荐使用这种方式。
23、网络质量实时回调
可以通过监听 onNetworkQuality 来实时统计本地及远端用户的网络质量,该回调每隔2秒抛出一次。
TRTCCloudListener( onNetworkQuality: (TRTCQualityInfo localQuality, List<TRTCQualityInfo> remoteQuality) { // localQuality userId 为空,代表本地用户网络质量评估结果 // remoteQuality 代表远端用户网络质量评估结果,其结果受远端和本地共同影响 switch (localQuality.quality) { case TRTCQuality.excellent: debugPrint("当前网络非常好"); break; case TRTCQuality.good: debugPrint("当前网络比较好"); break; case TRTCQuality.poor: debugPrint("当前网络一般"); break; case TRTCQuality.bad: debugPrint("当前网络较差"); break; case TRTCQuality.vBad: debugPrint("当前网络极差"); break; case TRTCQuality.down: debugPrint("当前网络不满足 TRTC 最低要求"); break; default: debugPrint("未定义"); break; } });
25、异常错误处理
TRTC SDK 遇到不可恢复的错误会在 onError 回调中抛出,详情请参见 TRTC 错误码表。
·UserSig 相关。UserSig 校验失败会导致进房失败,你可参见 UserSig 生成与校验 进行校验。
| 枚举 | 取值 | 描述 |
| ERR_USER_SIG_INVALID | -3320 | 进房参数 userSig 不正确 |
| ERR_SERVER_INFO_ECDH_GET_TINYID | -100018 | userSig 校验失败,请检查 userSig 是否正确 |
·进退房相关。进房失败请先检查进房参数是否正确,且进退房接口必须成对调用,即便进房失败也需要调用退房接口。
| 枚举 | 取值 | 描述 |
| ERR_ROOM_REQUEST_ENTER_ROOM_TIMEOUT | -3308 | 请求进房超时,请检查网络 |
| ERR_SDK_APPID_INVALID | -3317 | 进房参数 sdkAppId 错误 |
| ERR_ROOM_ID_INVALID | -3318 | 进房参数 roomId 错误 |
| ERR_USER_ID_INVALID | -3319 | 进房参数 userID 不正确 |
·设备相关。可监听设备相关错误,在出现相关错误时 UI 提示用户。
| 枚举 | 取值 | 描述 |
| ERR_MIC_START_FAIL | -1302 | 打开麦克风失败,例如在 Windows 或 Mac 设备,麦克风的配置程序(驱动程序)异常,禁用后重新启用设备,或者重启机器,或者更新配置程序 |
| ERR_SPEAKER_START_FAIL | -1321 | 打开扬声器失败,例如在 Windows 或 Mac 设备,扬声器的配置程序(驱动程序)异常,禁用后重新启用设备,或者重启机器,或者更新配置程序 |
| ERR_MIC_OCCUPY | -1319 | 麦克风正在被占用中,例如移动设备正在通话时,打开麦克风会失败。 |
26、异常退出处理
1. 断网感知与超时退房。
可以通过以下回调监听 TRTC 断网和重连事件通知。
收到 onConnectionLost 回调后可在本地麦位 UI 展示断网标识提醒用户,同时本地启动一个计时器,当超过设定时间阈值后仍然没有收到 onConnectionRecovery 回调,即网络持续处于断连状态,此时可本地启动下麦和退房流程,同时弹窗提醒用户已退出房间并销毁页面。若断网超过90秒(默认)会触发超时退房,TRTC 服务端会将该用户踢出房间,如果该用户为主播角色,则房间内其他用户会收到 onRemoteUserLeaveRoom 回调。
trtcCloud.registerListener( TRTCCloudListener( onConnectionLost: () { // SDK 与云端的连接已经断开 }, onTryToReconnect: () { // SDK 正在尝试重新连接到云端 }, onConnectionRecovery: () { // SDK 与云端的连接已经恢复 } ));
2. 离线状态下自动下麦。
IM 用户的普通状态分为在线(ONLINE)、离线(OFFLINE)、未登录(UNLOGINED),其中离线状态通常是由于用户强杀进程或网络异常中断导致的。你可以通过主播订阅连麦听众用户状态来检测离线连麦听众,从而将其踢下麦。
// 主播订阅连麦听众用户状态V2TimCallback subRes = await TencentImSDKPlugin.v2TIMManager.subscribeUserStatus(userIDList: userIDList);if (subRes.code == 0) { // 订阅用户状态成功} else { // 订阅用户状态失败}// 主播取消订阅下麦听众用户状态V2TimCallback unsubRes = await TencentImSDKPlugin.v2TIMManager.unsubscribeUserStatus(userIDList: userIDList);if (unsubRes.code == 0) { // 取消订阅用户状态成功} else { // 取消订阅用户状态失败}// 用户状态变更通知与处理TencentImSDKPlugin.v2TIMManager.addIMSDKListener( V2TimSDKListener( onUserStatusChanged: (List<V2TimUserStatus> userStatusList) { for (V2TimUserStatus userStatus in userStatusList) { final String? userId = userStatus.userID; int? status = userStatus.statusType; if (status == UserStatusType.V2TIM_USER_STATUS_OFFLINE) { // 离线状态执行踢麦 kickSeat(getSeatIndexFromUserId(userId)); } } } ));
注意:
27、服务端踢人及解散房间
1. 服务端踢人。
首先调用 TRTC 服务端踢人接口 RemoveUser(整型房间号)或 RemoveUserByStrRoomId(字符串房间号)将目标用户踢出 TRTC 房间,输入示例如下:
https://trtc.tencentcloudapi.com/?Action=RemoveUser&SdkAppId=1400000001&RoomId=1234&UserIds.0=test1&UserIds.1=test2&<公共请求参数>
执行踢人成功后,目标用户在客户端会收到 onExitRoom() 回调,且 reason 值为1。此时你可以在该回调中处理下麦、退出 IM 群组等操作。
TRTCCloudListener( // 离开 TRTC 房间事件回调 onExitRoom: (int reason) async { if (reason == 0) { debugPrint("主动调用 exitRoom 退出房间"); } else { // reason 1: 被服务器踢出当前房间 // reason 2: 当前房间被整个解散 debugPrint("被服务器踢出房间,或当前房间被解散"); // 下麦 leaveSeat(seatIndex); // 退出 IM 群组 V2TimCallback quitRes = await TencentImSDKPlugin.v2TIMManager.quitGroup(groupID: groupID); } });
2. 服务端解散房间。
首先调用 IM 服务端解散群组接口 destroy_group 解散目标群组,请求 URL 示例如下:
https://xxxxxx/v4/group_open_http_svc/destroy_group?sdkappid=88888888&identifier=admin&usersig=xxx&random=99999999&contenttype=json
执行解散群组成功后,目标群组内的全部成员在客户端均会收到 onGroupDismissed() 回调。此时你可以在该回调中处理退出 TRTC 房间等操作。
TencentImSDKPlugin.v2TIMManager.addGroupListener( listener: V2TimGroupListener( onGroupDismissed: (String groupID, V2TimGroupMemberInfo opUser,) { // 退出 TRTC 房间 trtcCloud.stopLocalAudio(); trtcCloud.exitRoom(); } ));
注意:说明:
注意:当房间内所有用户调用 exitRoom() 完成退房后,TRTC 房间会自动解散。当然,你也可以调用服务端接口 DismissRoom(整型房间号)或 DismissRoomByStrRoomId(字符串房间号)强制解散 TRTC 房间。
28、进房查看直播间历史消息
使用 AVChatRoom 默认不存储直播间历史消息,当新用户进入直播间后,只能看到进入直播间后用户发送的消息。为了优化新进群用户的体验,可在 即时通信 IM 控制台 配置直播群新成员查看入群前消息量:
开启后会在加入直播群成功后,在 onRecvNewMessage 回调中获得入群前消息。
TencentImSDKPlugin.v2TIMManager.getMessageManager().addAdvancedMsgListener( listener: V2TimAdvancedMsgListener( onRecvNewMessage: (V2TimMessage msg) { if (msg.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT) { String? sender = msg.sender; String? msgText = msg.textElem?.text; debugPrint("imsdk onRecvNewMessage, sender:${sender!}, msg:${msgText!}"); } } ));
注意:此功能仅旗舰版用户才可开通,且仅支持新入群成员查看入群前 24h 产生的消息,最多可查看 50 条。
29、进房感知麦上主播禁音状态
方案一:进房时默认所有主播为禁音状态,然后根据 onUserAudioAvailable(userId, true) 回调解除对应主播禁音状态。
TRTCCloudListener( onUserAudioAvailable: (String userId, bool available) { if (available) { // 解除对应主播的禁音状态 } });
方案二:把主播的禁音状态存储在 IM 群属性中,听众进房获取全量群属性,解析麦上主播禁音状态。
V2TimValueCallback<Map<String, String>> getRes = await TencentImSDKPlugin.v2TIMManager.getGroupManager().getGroupAttributes( groupID: groupID, keys: null );if (getRes.code == 0) { // 获取群属性成功,假设存储主播禁音状态的 key 为 muteStatus String muteStatus = getRes["muteStatus"]; // 解析 muteStatus,获取每个麦上主播的禁音状态} else { // 获取群属性失败}
30、音乐播放支持的资源路径问题
使用 TRTC SDK API startPlayMusic 播放背景音乐,其中音乐资源路径参数 path 不支持传入 Android 开发中的 assets/raw 等用来存储应用资源文件目录下的文件路径,因为这些目录下的文件会被打包到 APK 中,安装之后不会被解压到手机的文件系统中。目前只支持传入网络资源 URL、安卓设备外部存储及应用私有目录下资源文件的绝对路径。
你可以通过将 assets 目录下的资源文件提前拷贝到设备外部存储或应用私有目录下的方法规避这一问题,示例代码如下:
Future<void> copyAssetsToFile(String name) async { // 应用程序自身目录下的 files 目录 final savePath = await getExternalStorageDirectory(); // 应用程序自身目录下的 cache 目录 // final savePath = await getExternalCacheDirectories(); // 应用外部存储 files 目录路径 // final savePath = await getApplicationSupportDirectory(); String filename = "${savePath.path}/$name"; File file = File(savePath.path); // 如果目录不存在,创建这个目录 if (!(await file.exists())) { file.create(recursive: true); } try { if (!(await File(filename).exists())) { ByteData data = await rootBundle.load('assets/$name'); await file.writeAsBytes( data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes), flush: true, ); } } catch (error) { debugPrint(error.toString()); }}
·应用私有存储 files 目录路径:/data/user/0/<package_name>/files/<file_name>
·应用外部存储 files 目录路径:/storage/emulated/0/Android/data/<package_name>/files/<file_name>
·应用外部存储 cache 目录路径:/storage/emulated/0/Android/data/<package_name>/cache/file_name>
注意:<manifest …> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <application …> … </application></manifest>
注意:final androidInfo = await DeviceInfoPlugin().deviceInfo;if (androidInfo.data["version"]["sdkInt"] >= 30) { // Android 11 以上,动态申请权限 await Permission.manageExternalStorage.request();} else { // 跳转到设置页面,引导用户手动授权 final intent = AndroidIntent( action: 'android.settings.MANAGE_APP_ALL_FILES_ACCESS_PERMISSION', data: 'package:${androidInfo.data["name"]}', ); await intent.launch();}
31、主播闭麦状态下播放音乐,远端用户听不到
通常情况下,本地闭麦操作需要调用 muteLocalAudio 方法,但在播放背景音乐时调用此方法会暂停将音乐发布至远端,即远端用户听不到背景音乐了。如果希望在闭麦的同时能够正常播放和发布背景音乐,可以参见下述方案实现。
// 1. 获取当前的人声采集音量int volume = trtcCloud.getAudioCaptureVolume();// 2. 将人声采集音量设为0,达到闭麦效果trtcCloud.setAudioCaptureVolume(0); // 3. 解除闭麦时,恢复至原先的人声采集音量trtcCloud.setAudioCaptureVolume(volume);