Commit 97536e98 authored by Alex朱枝文's avatar Alex朱枝文

更改腾讯会议录屏功能

parent c8fe5e0c
# Quick Run of TUIRoomKit Demo for iOS
_[中文](README.md) | English_
This document describes how to quickly run the TUIRoomKit demo project to make a high-quality audio/video call. For more information on the TUIRoomKit component connection process, see **[Integrating TUIRoomKit (iOS)](https://cloud.tencent.com/document/product/647/84237)**.
## Directory Structure
```
TUIRoomKit
├─ Example // multi-person video conferencing demo project
├─ App // Folder of entering/creating multi-person video conferencing UI code and used images and internationalization string resources
├─ Debug // Folder of the key business code required for project debugging and running
├─ Login // Folder of the login UI and business logic code
└─ TXReplayKit_Screen // Folder of sharing screen
├─ TUIRoomKit // Folder of multi-person video conferencing UI code and used images and internationalization string resources
├─ TUIBarrage // Barrage components
└─ TUIBeauty // Beauty components
```
## Environment Requirements
- Xcode 12.0 or above
-
## Running the Demo
[](id:ui.step1)
### Step 1. Create a TRTC application
1. Go to the [Application management](https://console.cloud.tencent.com/trtc/app) page in the TRTC console, select **Create Application**, enter an application name such as `TUIKitDemo`, and click **Confirm**.
2. Click **Application Information** on the right of the application as shown below:
<img src="https://qcloudimg.tencent-cloud.cn/raw/62f58d310dde3de2d765e9a460b8676a.png" width="900">
3. On the application information page, note down the `SDKAppID` and key as shown below:
<img src="https://qcloudimg.tencent-cloud.cn/raw/bea06852e22a33c77cb41d287cac25db.png" width="900">
>! This feature uses two basic PaaS services of Tencent Cloud: [TRTC](https://www.tencentcloud.com/document/product/647/35078) and [IM](https://www.tencentcloud.com/document/product/1047/33513). When you activate TRTC, IM will be activated automatically. IM is a value-added service.
[](id:ui.step2)
### Step 2. Configure the project
1. Open the demo project `DemoApp.xcworkspace` with Xcode 12.0 or later.
2. Find the `iOS/Example/Debug/GenerateTestUserSig.swift` file in the project.
3. Set the following parameters in `GenerateTestUserSig.swift`:
<ul style="margin:0"><li/>SDKAPPID: `0` by default. Set it to the actual `SDKAppID`.
<li/>SECRETKEY: Left empty by default. Set it to the actual key.</ul>
![](https://qcloudimg.tencent-cloud.cn/raw/1c4eb799c7e06aa2da54ece87ccf993e.png)
[](id:ui.step3)
### Step 3. Compile and run the application
1. Open Terminal, enter the project directory, run the `pod install` command, and wait for it to complete.
2. Open the demo project `TUIRoomKit/Example/DemoApp.xcworkspace` with Xcode 12.0 or later and click **Run**.
[](id:ui.step4)
## Have any questions?
Welcome to join our Telegram Group to communicate with our professional engineers! We are more than happy to hear from you~
Click to join: https://t.me/+EPk6TMZEZMM5OGY1
Or scan the QR code
<img src="https://qcloudimg.tencent-cloud.cn/raw/9c67ed5746575e256b81ce5a60216c5a.jpg" width="320"/>
# TUIRoomKit iOS 示例工程快速跑通
_中文 | [English](README.en.md)_
本文档主要介绍如何快速跑通TUIRoomKit 示例工程,体验高质量多人视频会议,更详细的TUIRoomKit组件接入流程,请点击腾讯云官网文档: [**TUIRoomKit 组件 iOS 接入说明** ](https://cloud.tencent.com/document/product/647/84237)...
![](https://qcloudimg.tencent-cloud.cn/raw/b847f8497287077db8909503e6880e19.svg)
## 目录结构
```
TUIRoomKit
├─ Example // 多人视频会议Demo工程
├─ App // 进入/创建多人视频会议UI代码以及用到的图片及国际化字符串资源文件夹
├─ Debug // 工程调试运行所需的关键业务代码文件夹
├─ Login // 登录UI及业务逻辑代码文件夹
└─ TXReplayKit_Screen // 共享屏幕逻辑代码文件夹
├─ TUIRoomKit // 多人视频会议主要UI代码以及所需的图片、国际化字符串资源文件夹
├─ TUIBarrage // 弹幕组件
└─ TUIBeauty // 美颜组件
```
## 环境准备
iOS 12.0及更高。
## 运行并体验 App
[](id:ui.step1)
### 第一步:创建TRTC的应用
TUIRoomKit 是基于腾讯云 [即时通信 IM](https://cloud.tencent.com/document/product/269/42440)[实时音视频 TRTC](https://cloud.tencent.com/document/product/647/16788) 两项付费 PaaS 服务构建出的音视频通信组件。您可以按照如下步骤开通相关的服务:
1. 登录到 [即时通信 IM 控制台](https://console.cloud.tencent.com/im),单击**创建新应用**,在弹出的对话框中输入您的应用名称,并单击**确定**
![](https://qcloudimg.tencent-cloud.cn/raw/07fa9407da05b76b3dbbd9d2c4714cc8.png)
2. 单击刚刚创建出的应用,进入基本配置页面,并在页面的右下角找到开通腾讯实时音视频服务功能区,单击立即开通即可开通实时音视频TRTC 的 7 天免费试用服务。如果需要正式应用上线,可以前往[**实时音视频控制台**](https://console.cloud.tencent.com/trtc/app)付费购买正式版本。
![](https://qcloudimg.tencent-cloud.cn/raw/daa624cbc9c87c787f2afc5b37a8f272.png)
> **友情提示**:
> 单击 **免费体验** 以后,部分之前使用过 [实时音视频 TRTC](https://cloud.tencent.com/document/product/647/16788) 服务的用户会提示:
> ```java
> [-100013]:TRTC service is suspended. Please check if the package balance is 0 or the Tencent Cloud accountis in arrears
> ```
> 因为新的 IM 音视频通话能力是整合了腾讯云 [实时音视频 TRTC](https://cloud.tencent.com/document/product/647/16788) 和 [即时通信 IM](https://cloud.tencent.com/document/product/269/42440) 两个基础的 PaaS 服务,所以当 [实时音视频 TRTC](https://cloud.tencent.com/document/product/647/16788) 的免费额度(10000分钟)已经过期或者耗尽,就会导致开通此项服务失败,这里您可以单击 [TRTC 控制台](https://console.cloud.tencent.com/trtc/app),找到对应 SDKAppID 的应用管理页,示例如图,开通后付费功能后,再次 **启用应用** 即可正常体验音视频通话能力。
![](https://qcloudimg.tencent-cloud.cn/raw/559f87a883348cf27cf6ac202f769243.png)
3. 进入应用信息后,按下图操作,记录SDKAppID和密钥:
![](https://qcloudimg.tencent-cloud.cn/raw/ca696884bd53233447b22c730ed82205.png)
[](id:ui.step2)
### 第二步:配置工程
1. 使用Xcode(12.0及以上)打开源码工程`DemoApp.xcworkspace`
2. 工程内找到 `iOS/Example/Debug/GenerateTestUserSig.swift` 文件。
3. 设置 `GenerateTestUserSig.swift` 文件中的相关参数:
<ul style="margin:0"><li/>SDKAPPID:默认为0,请设置为实际的 SDKAppID。
<li/>SECRETKEY:默认为空字符串,请设置为实际的密钥信息。</ul>
![](https://qcloudimg.tencent-cloud.cn/raw/1c4eb799c7e06aa2da54ece87ccf993e.png)
[](id:ui.step3)
### 第三步:编译运行
1. 打开Terminal(终端)进入到工程目录下执行`pod install`指令,等待完成。
2. Xcode(12.0及以上的版本)打开源码工程 `TUIRoomKit/iOS/Example/DemoApp.xcworkspace`,单击 **运行** 即可开始调试本 App。
[](id:ui.step4)
>? 如果您在使用过程中,有什么建议或者意见,欢迎您加入我们的 TUIKit 组件交流群 QQ 群:592465424,进行技术交流和产品沟通。
{
"images" : [
{
"filename" : "mic_bottom.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "mic_bottom@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "mic_bottom@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "room_modify_name.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "room_modify_name@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "room_modify_name@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "room_mute_audio1.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "room_mute_audio1@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "room_mute_audio1@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "room_mute_share_screen.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "room_mute_share_screen@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "room_mute_share_screen@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "room_unmute_share_screen.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "room_unmute_share_screen@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "room_unmute_share_screen@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
...@@ -14,7 +14,7 @@ class InviteToJoinRoomManager { ...@@ -14,7 +14,7 @@ class InviteToJoinRoomManager {
let inviteJoinModel = InviteJoinModel(message: message, inviter: inviter) let inviteJoinModel = InviteJoinModel(message: message, inviter: inviter)
pushSelectGroupMemberViewController(groupId: message.groupId) { responseData in pushSelectGroupMemberViewController(groupId: message.groupId) { responseData in
guard let modelList = guard let modelList =
responseData[TUICore_TUIGroupObjectFactory_SelectGroupMemberVC_ResultUserList] as? [TUIUserModel] responseData[TUICore_TUIContactObjectFactory_SelectGroupMemberVC_ResultUserList] as? [TUIUserModel]
else { return } else { return }
var invitedList: [String] = [] var invitedList: [String] = []
for userModel in modelList { for userModel in modelList {
...@@ -25,15 +25,15 @@ class InviteToJoinRoomManager { ...@@ -25,15 +25,15 @@ class InviteToJoinRoomManager {
} }
class func pushSelectGroupMemberViewController(groupId: String, callback: @escaping TUIValueResultCallback) { class func pushSelectGroupMemberViewController(groupId: String, callback: @escaping TUIValueResultCallback) {
let param = [TUICore_TUIGroupObjectFactory_SelectGroupMemberVC_GroupID: groupId] let param = [TUICore_TUIContactObjectFactory_SelectGroupMemberVC_GroupID: groupId]
if let navigateController = RoomMessageManager.shared.navigateController { if let navigateController = RoomMessageManager.shared.navigateController {
navigateController.push(TUICore_TUIGroupObjectFactory_SelectGroupMemberVC_Classic, param: param) { responseData in navigateController.push(TUICore_TUIContactObjectFactory_SelectGroupMemberVC_Classic, param: param) { responseData in
callback(responseData) callback(responseData)
} }
} else { } else {
let nav = UINavigationController() let nav = UINavigationController()
let currentViewController = getCurrentWindowViewController() let currentViewController = getCurrentWindowViewController()
currentViewController?.present(TUICore_TUIGroupObjectFactory_SelectGroupMemberVC_Classic, currentViewController?.present(TUICore_TUIContactObjectFactory_SelectGroupMemberVC_Classic,
param: param, embbedIn: nav, param: param, embbedIn: nav,
forResult: { responseData in forResult: { responseData in
callback(responseData) callback(responseData)
...@@ -47,7 +47,7 @@ class InviteToJoinRoomManager { ...@@ -47,7 +47,7 @@ class InviteToJoinRoomManager {
guard let dataString = dataDict.convertToString() else { return } guard let dataString = dataDict.convertToString() else { return }
let pushInfo = V2TIMOfflinePushInfo() let pushInfo = V2TIMOfflinePushInfo()
invitedList.forEach { userId in invitedList.forEach { userId in
V2TIMManager.sharedInstance().invite(userId, V2TIMManager.sharedInstance().invite(invitee: userId,
data: dataString, data: dataString,
onlineUserOnly: true, onlineUserOnly: true,
offlinePushInfo: pushInfo, offlinePushInfo: pushInfo,
......
...@@ -38,24 +38,22 @@ class RoomManager { ...@@ -38,24 +38,22 @@ class RoomManager {
engineManager.createRoom(roomInfo: roomInfo) { [weak self] in engineManager.createRoom(roomInfo: roomInfo) { [weak self] in
guard let self = self else { return } guard let self = self else { return }
self.roomObserver.createdRoom() self.roomObserver.createdRoom()
self.enterRoom(roomId: roomInfo.roomId, isShownConferenceViewController: false) self.enterRoom(roomId: roomInfo.roomId)
} onError: { _, message in } onError: { _, message in
RoomRouter.makeToast(toast: message) RoomRouter.makeToast(toast: message)
} }
} }
func enterRoom(roomId: String, isShownConferenceViewController: Bool = true) { func enterRoom(roomId: String, onSuccess: TUIRoomInfoBlock? = nil, onError: TUIErrorBlock? = nil) {
roomObserver.registerObserver() roomObserver.registerObserver()
engineManager.store.isImAccess = true engineManager.store.isImAccess = true
self.roomId = roomId self.roomId = roomId
engineManager.enterRoom(roomId: roomId, enableAudio: engineManager.store.isOpenMicrophone, enableVideo: engineManager.store.isOpenCamera, isSoundOnSpeaker: true) { [weak self] roomInfo in engineManager.enterRoom(roomId: roomId, enableAudio: engineManager.store.isOpenMicrophone, enableVideo: engineManager.store.isOpenCamera, isSoundOnSpeaker: true) { [weak self] roomInfo in
guard let self = self else { return } guard let self = self else { return }
self.roomObserver.enteredRoom() self.roomObserver.enteredRoom()
guard isShownConferenceViewController else { return } onSuccess?(roomInfo)
let vc = ConferenceMainViewController() } onError: { code, message in
RoomRouter.shared.push(viewController: vc) onError?(code, message)
} onError: { _, message in
RoomRouter.makeToast(toast: message)
} }
} }
......
...@@ -59,12 +59,12 @@ class RoomMessageManager: NSObject { ...@@ -59,12 +59,12 @@ class RoomMessageManager: NSObject {
let messageModel = RoomMessageModel() let messageModel = RoomMessageModel()
messageModel.groupId = self.groupId messageModel.groupId = self.groupId
messageModel.roomId = roomId messageModel.roomId = roomId
messageModel.ownerName = TUILogin.getNickName() ?? "" messageModel.ownerName = TUILogin.getNickName() ?? self.userId
messageModel.owner = self.userId messageModel.owner = self.userId
let messageDic = messageModel.getDictFromMessageModel() let messageDic = messageModel.getDictFromMessageModel()
guard let jsonString = messageDic.convertToString() else { return } guard let jsonString = messageDic.convertToString() else { return }
let jsonData = jsonString.data(using: String.Encoding.utf8) let jsonData = jsonString.data(using: String.Encoding.utf8)
let message = V2TIMManager.sharedInstance().createCustomMessage(jsonData) let message = V2TIMManager.sharedInstance().createCustomMessage(data: jsonData)
message?.supportMessageExtension = true message?.supportMessageExtension = true
let param = [TUICore_TUIChatService_SendMessageMethod_MsgKey: message] let param = [TUICore_TUIChatService_SendMessageMethod_MsgKey: message]
TUICore.callService(TUICore_TUIChatService, method: TUICore_TUIChatService_SendMessageMethod, param: param as [AnyHashable : Any]) TUICore.callService(TUICore_TUIChatService, method: TUICore_TUIChatService_SendMessageMethod, param: param as [AnyHashable : Any])
...@@ -78,7 +78,7 @@ class RoomMessageManager: NSObject { ...@@ -78,7 +78,7 @@ class RoomMessageManager: NSObject {
self.modifyMessage(message: message, dic: dic) self.modifyMessage(message: message, dic: dic)
return return
} }
V2TIMManager.sharedInstance().findMessages([message.messageId]) { [weak self] messageArray in V2TIMManager.sharedInstance().findMessages(messageIDList: [message.messageId]) { [weak self] messageArray in
guard let self = self else { return } guard let self = self else { return }
guard let array = messageArray else { return } guard let array = messageArray else { return }
for previousMessage in array where previousMessage.msgID == message.messageId { for previousMessage in array where previousMessage.msgID == message.messageId {
...@@ -92,14 +92,14 @@ class RoomMessageManager: NSObject { ...@@ -92,14 +92,14 @@ class RoomMessageManager: NSObject {
private func modifyMessage(message: RoomMessageModel, dic:[String: Any]) { private func modifyMessage(message: RoomMessageModel, dic:[String: Any]) {
guard let message = message.getMessage() else { return } guard let message = message.getMessage() else { return }
guard var customElemDic = TUITool.jsonData2Dictionary(message.customElem.data) as? [String: Any] else { return } guard var customElemDic = TUITool.jsonData2Dictionary(message.customElem?.data) as? [String: Any] else { return }
for (key, value) in dic { for (key, value) in dic {
customElemDic[key] = value customElemDic[key] = value
} }
guard let jsonString = customElemDic.convertToString() else { return } guard let jsonString = customElemDic.convertToString() else { return }
let jsonData = jsonString.data(using: String.Encoding.utf8) let jsonData = jsonString.data(using: String.Encoding.utf8)
message.customElem.data = jsonData message.customElem?.data = jsonData
V2TIMManager.sharedInstance().modifyMessage(message) { code, desc, msg in V2TIMManager.sharedInstance().modifyMessage(msg: message) { code, desc, msg in
if code == 0 { if code == 0 {
debugPrint("modifyMessage,success") debugPrint("modifyMessage,success")
} else { } else {
......
...@@ -62,7 +62,7 @@ class RoomMessageModel { ...@@ -62,7 +62,7 @@ class RoomMessageModel {
private func setMessageCustomElemData(dict: [String: Any], message: V2TIMMessage) { private func setMessageCustomElemData(dict: [String: Any], message: V2TIMMessage) {
guard let jsonString = dict.convertToString() else { return } guard let jsonString = dict.convertToString() else { return }
let jsonData = jsonString.data(using: String.Encoding.utf8) let jsonData = jsonString.data(using: String.Encoding.utf8)
message.customElem.data = jsonData message.customElem?.data = jsonData
} }
func getDictFromMessageModel() -> [String: Any] { func getDictFromMessageModel() -> [String: Any] {
......
...@@ -76,7 +76,7 @@ extension RoomMessageExtensionObserver: TUIExtensionProtocol { ...@@ -76,7 +76,7 @@ extension RoomMessageExtensionObserver: TUIExtensionProtocol {
private extension String { private extension String {
static var meetingText: String { static var meetingText: String {
localized("Quick meeting") localized("Quick conference")
} }
static var roomDeviceSetText: String { static var roomDeviceSetText: String {
localized("Meeting Settings") localized("Meeting Settings")
......
...@@ -33,7 +33,7 @@ class TUIRoomImAccessService: NSObject, TUIServiceProtocol { ...@@ -33,7 +33,7 @@ class TUIRoomImAccessService: NSObject, TUIServiceProtocol {
} }
extension TUIRoomImAccessService: V2TIMSignalingListener { extension TUIRoomImAccessService: V2TIMSignalingListener {
func onReceiveNewInvitation(_ inviteID: String, inviter: String, groupID: String, inviteeList: [String], data: String?) { func onReceiveNewInvitation(inviteID: String, inviter: String?, groupID: String?, inviteeList: [String], data: String?) {
guard let data = data else { return } guard let data = data else { return }
let dict = data.convertToDic() let dict = data.convertToDic()
guard let businessID = dict?["businessID"] as? String else { return } guard let businessID = dict?["businessID"] as? String else { return }
......
...@@ -173,7 +173,7 @@ class RoomMessageView: UIView { ...@@ -173,7 +173,7 @@ class RoomMessageView: UIView {
} }
func setupViewState() { func setupViewState() {
roomNameLabel.text = (viewModel.message.ownerName ) + .quickMeetingText roomNameLabel.text = localizedReplace(.quickMeetingText, replace: viewModel.message.ownerName)
if viewModel.message.owner != viewModel.userId { if viewModel.message.owner != viewModel.userId {
inviteUserButton.isHidden = true inviteUserButton.isHidden = true
} else { } else {
...@@ -322,7 +322,5 @@ private extension String { ...@@ -322,7 +322,5 @@ private extension String {
static var meetingEnded: String { static var meetingEnded: String {
localized("Meeting ended") localized("Meeting ended")
} }
static var quickMeetingText: String { static let quickMeetingText = localized("xx's quick conference")
localized("Quick meeting")
}
} }
...@@ -39,9 +39,9 @@ class ChatExtensionRoomSettingsViewModel { ...@@ -39,9 +39,9 @@ class ChatExtensionRoomSettingsViewModel {
} }
private extension String { private extension String {
static var cameraSetText: String { static var cameraSetText: String {
localized("Join the meeting and start the camera") localized("Enable video when joining a meeting")
} }
static var micSeatText: String { static var micSeatText: String {
localized("Join the conference and turn on the mic") localized("Enable audio when joining a meeting")
} }
} }
...@@ -60,7 +60,12 @@ class InvitedToJoinRoomViewModel: NSObject, AVAudioPlayerDelegate { ...@@ -60,7 +60,12 @@ class InvitedToJoinRoomViewModel: NSObject, AVAudioPlayerDelegate {
} }
private func enterRoom() { private func enterRoom() {
roomManager.enterRoom(roomId: roomId) roomManager.enterRoom(roomId: roomId) {_ in
let vc = ConferenceMainViewController()
RoomRouter.shared.push(viewController: vc)
} onError: { code, message in
RoomRouter.makeToast(toast: code.description ?? message)
}
closeInvitedToJoinRoomView() closeInvitedToJoinRoomView()
} }
......
...@@ -31,9 +31,9 @@ class RoomMessageBubbleCellData: TUIBubbleMessageCellData { ...@@ -31,9 +31,9 @@ class RoomMessageBubbleCellData: TUIBubbleMessageCellData {
override class func getDisplayString(_ message: V2TIMMessage) -> String { override class func getDisplayString(_ message: V2TIMMessage) -> String {
let businessID = parseBusinessID(message: message) let businessID = parseBusinessID(message: message)
if businessID == BussinessID_GroupRoomMessage { if businessID == BussinessID_GroupRoomMessage {
let dict = TUITool.jsonData2Dictionary(message.customElem.data) as? [String: Any] let dict = TUITool.jsonData2Dictionary(message.customElem?.data) as? [String: Any]
let userName = dict?["ownerName"] as? String ?? "" let userName = dict?["ownerName"] as? String ?? ""
return userName + .quickMeetingText return localizedReplace(.quickMeetingText, replace: userName)
} else { } else {
return super.getDisplayString(message) return super.getDisplayString(message)
} }
...@@ -41,7 +41,7 @@ class RoomMessageBubbleCellData: TUIBubbleMessageCellData { ...@@ -41,7 +41,7 @@ class RoomMessageBubbleCellData: TUIBubbleMessageCellData {
private class func parseBusinessID(message: V2TIMMessage?) -> String { private class func parseBusinessID(message: V2TIMMessage?) -> String {
guard let message = message else { return "" } guard let message = message else { return "" }
let customData = message.customElem.data let customData = message.customElem?.data
let dict = TUITool.jsonData2Dictionary(customData) let dict = TUITool.jsonData2Dictionary(customData)
guard let businessID = dict?["businessID"] as? String else { return ""} guard let businessID = dict?["businessID"] as? String else { return ""}
return businessID return businessID
...@@ -54,6 +54,6 @@ class RoomMessageBubbleCellData: TUIBubbleMessageCellData { ...@@ -54,6 +54,6 @@ class RoomMessageBubbleCellData: TUIBubbleMessageCellData {
private extension String { private extension String {
static var quickMeetingText: String { static var quickMeetingText: String {
localized("'s quick meeting") localized("xx's quick conference")
} }
} }
...@@ -66,7 +66,15 @@ class RoomMessageViewModel: NSObject { ...@@ -66,7 +66,15 @@ class RoomMessageViewModel: NSObject {
private func enterRoom() { private func enterRoom() {
if !engineManager.store.isEnteredRoom { if !engineManager.store.isEnteredRoom {
roomManager.enterRoom(roomId: message.roomId) roomManager.enterRoom(roomId: message.roomId) {_ in
let vc = ConferenceMainViewController()
RoomRouter.shared.push(viewController: vc)
} onError: { [weak self] code, errorMessage in
if let self = self, code == .roomIdNotExist {
self.messageManager.resendRoomMessage(message: self.message, dic: ["roomState": RoomMessageModel.RoomState.destroyed.rawValue])
}
RoomRouter.makeToast(toast: code.description ?? errorMessage)
}
} else { } else {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ShowRoomMainView, param: [:]) EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ShowRoomMainView, param: [:])
} }
......
...@@ -15,12 +15,17 @@ import Combine ...@@ -15,12 +15,17 @@ import Combine
#endif #endif
import Factory import Factory
protocol FloatChatDisplayViewDelegate: AnyObject {
func getTheLatestUserName(userId: String) -> String
}
class FloatChatDisplayView: UIView { class FloatChatDisplayView: UIView {
@Injected(\.floatChatService) private var store: FloatChatStoreProvider @Injected(\.floatChatService) private var store: FloatChatStoreProvider
private lazy var messagePublisher = self.store.select(FloatChatSelectors.getLatestMessage) private lazy var messagePublisher = self.store.select(FloatChatSelectors.getLatestMessage)
private var messages: [FloatChatMessageView] = [] private var messages: [FloatChatMessageView] = []
var cancellableSet = Set<AnyCancellable>() var cancellableSet = Set<AnyCancellable>()
private let messageSpacing: CGFloat = 8 private let messageSpacing: CGFloat = 8
weak var delegate: FloatChatDisplayViewDelegate?
private lazy var blurLayer: CALayer = { private lazy var blurLayer: CALayer = {
let layer = CAGradientLayer() let layer = CAGradientLayer()
...@@ -46,6 +51,7 @@ class FloatChatDisplayView: UIView { ...@@ -46,6 +51,7 @@ class FloatChatDisplayView: UIView {
guard !isViewReady else { return } guard !isViewReady else { return }
constructViewHierarchy() constructViewHierarchy()
bindInteraction() bindInteraction()
reportViewShow()
isViewReady = true isViewReady = true
} }
...@@ -55,7 +61,7 @@ class FloatChatDisplayView: UIView { ...@@ -55,7 +61,7 @@ class FloatChatDisplayView: UIView {
func bindInteraction() { func bindInteraction() {
messagePublisher messagePublisher
.filter{ !$0.content.isEmpty } .filter{ !($0.content.isEmpty && $0.type == .text) }
.receive(on: DispatchQueue.mainQueue) .receive(on: DispatchQueue.mainQueue)
.sink { [weak self] floatMessage in .sink { [weak self] floatMessage in
guard let self = self else { return } guard let self = self else { return }
...@@ -64,7 +70,15 @@ class FloatChatDisplayView: UIView { ...@@ -64,7 +70,15 @@ class FloatChatDisplayView: UIView {
.store(in: &cancellableSet) .store(in: &cancellableSet)
} }
private func reportViewShow() {
store.dispatch(action: FloatChatActions.reportData(payload: .metricsBarragePanelShow))
}
private func addMessage(_ message: FloatChatMessage) { private func addMessage(_ message: FloatChatMessage) {
var message = message
if let userName = delegate?.getTheLatestUserName(userId: message.user.userId), !userName.isEmpty {
message.user.userName = userName
}
let messageView = FloatChatMessageView(floatMessage: message) let messageView = FloatChatMessageView(floatMessage: message)
if currentMessageHeight() + messageView.height + messageSpacing > bounds.height { if currentMessageHeight() + messageView.height + messageSpacing > bounds.height {
removeOldestMessage() removeOldestMessage()
......
...@@ -170,6 +170,7 @@ class FloatChatInputController: UIViewController { ...@@ -170,6 +170,7 @@ class FloatChatInputController: UIViewController {
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .inputCannotBeEmpty, position: .center))) operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .inputCannotBeEmpty, position: .center)))
} else { } else {
store.dispatch(action: FloatChatActions.sendMessage(payload: inputTextView.normalText)) store.dispatch(action: FloatChatActions.sendMessage(payload: inputTextView.normalText))
store.dispatch(action: FloatChatActions.reportData(payload: .metricsBarrageSendMessage))
} }
hideInputView() hideInputView()
} }
......
...@@ -27,13 +27,13 @@ class FloatChatService: NSObject { ...@@ -27,13 +27,13 @@ class FloatChatService: NSObject {
override init() { override init() {
super.init() super.init()
imManager?.addSimpleMsgListener(listener: self) imManager?.addAdvancedMsgListener(listener: self)
} }
func sendGroupMessage(_ message: String) -> AnyPublisher<String, Never> { func sendGroupMessage(_ message: String) -> AnyPublisher<String, Never> {
return Future<String, Never> { [weak self] promise in return Future<String, Never> { [weak self] promise in
guard let self = self else { return } guard let self = self else { return }
self.imManager?.sendGroupTextMessage(message, to: self.roomId, priority: .PRIORITY_NORMAL, succ: { self.imManager?.sendGroupTextMessage(text: message, to: self.roomId, priority: .PRIORITY_NORMAL, succ: {
promise(.success((message))) promise(.success((message)))
}, fail: { code, message in }, fail: { code, message in
let errorMsg = TUITool.convertIMError(Int(code), msg: message) let errorMsg = TUITool.convertIMError(Int(code), msg: message)
...@@ -45,13 +45,10 @@ class FloatChatService: NSObject { ...@@ -45,13 +45,10 @@ class FloatChatService: NSObject {
} }
} }
extension FloatChatService: V2TIMSimpleMsgListener { extension FloatChatService: V2TIMAdvancedMsgListener {
func onRecvGroupTextMessage(_ msgID: String!, groupID: String!, sender info: V2TIMGroupMemberInfo!, text: String!) { func onRecvNewMessage(msg: V2TIMMessage!) {
guard groupID == roomId else { guard msg.groupID == roomId, let msg = msg else { return }
return let floatMessage = FloatChatMessage(msg: msg)
}
let user = FloatChatUser(memberInfo: info)
let floatMessage = FloatChatMessage(user: user, content: text)
store?.dispatch(action: FloatChatActions.onMessageReceived(payload: floatMessage)) store?.dispatch(action: FloatChatActions.onMessageReceived(payload: floatMessage))
} }
} }
...@@ -15,10 +15,19 @@ struct FloatChatState: Codable { ...@@ -15,10 +15,19 @@ struct FloatChatState: Codable {
var latestMessage = FloatChatMessage() var latestMessage = FloatChatMessage()
} }
enum FloatChatMessageType: Codable, Equatable {
case text
case image
case video
case file
}
struct FloatChatMessage: Codable, Equatable { struct FloatChatMessage: Codable, Equatable {
var id = UUID() var id = UUID()
var user = FloatChatUser() var user = FloatChatUser()
var type: FloatChatMessageType = .text
var content: String = "" var content: String = ""
var fileName: String = ""
var extInfo: [String: AnyCodable] = [:] var extInfo: [String: AnyCodable] = [:]
init() {} init() {}
...@@ -27,6 +36,23 @@ struct FloatChatMessage: Codable, Equatable { ...@@ -27,6 +36,23 @@ struct FloatChatMessage: Codable, Equatable {
self.user = user self.user = user
self.content = content self.content = content
} }
init(msg: V2TIMMessage) {
self.user = FloatChatUser(userId: msg.sender ?? "", userName: msg.nickName ?? "", avatarUrl: msg.faceURL ?? "")
switch msg.elemType {
case .ELEM_TYPE_TEXT:
self.type = .text
self.content = msg.textElem?.text ?? ""
case .ELEM_TYPE_IMAGE:
self.type = .image
case .ELEM_TYPE_VIDEO:
self.type = .video
case .ELEM_TYPE_FILE:
self.type = .file
self.fileName = msg.fileElem?.filename ?? ""
default: break
}
}
} }
struct FloatChatUser: Codable, Equatable { struct FloatChatUser: Codable, Equatable {
......
...@@ -14,6 +14,7 @@ enum FloatChatActions { ...@@ -14,6 +14,7 @@ enum FloatChatActions {
static let onMessageSended = ActionTemplate(id: key.appending(".messageSended"), payloadType: String.self) static let onMessageSended = ActionTemplate(id: key.appending(".messageSended"), payloadType: String.self)
static let onMessageReceived = ActionTemplate(id: key.appending(".messageReceived"), payloadType: FloatChatMessage.self) static let onMessageReceived = ActionTemplate(id: key.appending(".messageReceived"), payloadType: FloatChatMessage.self)
static let setRoomId = ActionTemplate(id: key.appending(".setRoomId"), payloadType: String.self) static let setRoomId = ActionTemplate(id: key.appending(".setRoomId"), payloadType: String.self)
static let reportData = ActionTemplate(id: key.appending(".reportData"), payloadType: DataReport.self)
} }
enum FloatViewActions { enum FloatViewActions {
......
...@@ -18,4 +18,12 @@ class FloatChatEffect: Effects { ...@@ -18,4 +18,12 @@ class FloatChatEffect: Effects {
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
let reportData = Effect<Environment>.nonDispatching { actions, environment in
actions
.wasCreated(from: FloatChatActions.reportData)
.sink { action in
RoomKitReport.reportData(action.payload)
}
}
} }
...@@ -84,11 +84,25 @@ class FloatChatMessageView: UIView { ...@@ -84,11 +84,25 @@ class FloatChatMessageView: UIView {
let userNameAttributedText = NSMutableAttributedString(string: userName, let userNameAttributedText = NSMutableAttributedString(string: userName,
attributes: userNameAttributes) attributes: userNameAttributes)
let contentAttributedText: NSMutableAttributedString = getFullContentAttributedText(content: message.content) let content = getDisplayedContent(message: message)
let contentAttributedText: NSMutableAttributedString = getFullContentAttributedText(content: content)
userNameAttributedText.append(contentAttributedText) userNameAttributedText.append(contentAttributedText)
return userNameAttributedText return userNameAttributedText
} }
private func getDisplayedContent(message: FloatChatMessage) -> String {
switch message.type {
case .text:
return message.content
case .image:
return .sentPicture
case .video:
return .sentVideo
case .file:
return localizedReplace(.sent, replace: message.fileName)
}
}
private func getFullContentAttributedText(content: String) -> NSMutableAttributedString { private func getFullContentAttributedText(content: String) -> NSMutableAttributedString {
return EmotionHelper.shared.obtainImagesAttributedString(byText: content, return EmotionHelper.shared.obtainImagesAttributedString(byText: content,
font: UIFont(name: "PingFangSC-Regular", size: 12) ?? font: UIFont(name: "PingFangSC-Regular", size: 12) ??
...@@ -96,3 +110,9 @@ class FloatChatMessageView: UIView { ...@@ -96,3 +110,9 @@ class FloatChatMessageView: UIView {
} }
} }
private extension String {
static let sentPicture = localized("Sent a picture")
static let sentVideo = localized("Sent a video")
static let sent = localized("Sent xx")
}
//
// VideoSeatCell.swift
// Pods
//
// Created by CY zhao on 2024/11/11.
//
import SnapKit
import UIKit
import Combine
class MultiStreamCell: UICollectionViewCell {
var cancellableSet = Set<AnyCancellable>()
var videoItem: UserInfo?
var isSupportedAmplification: Bool {
return videoItem?.videoStreamType == .screenStream
}
private var isBorderHighlighted = false
private var lastVolumeUpdateTime: TimeInterval = 0
private lazy var scrollRenderView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.backgroundColor = UIColor(0x17181F)
scrollView.layer.cornerRadius = 16
scrollView.layer.masksToBounds = true
scrollView.layer.borderWidth = 2
scrollView.layer.borderColor = UIColor.clear.cgColor
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
scrollView.maximumZoomScale = 5
scrollView.minimumZoomScale = 1
scrollView.isScrollEnabled = false
scrollView.delegate = self
return scrollView
}()
let renderView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
return view
}()
let backgroundMaskView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = UIColor(0x17181F)
view.layer.cornerRadius = 16
view.layer.masksToBounds = true
return view
}()
let userInfoView: VideoUserStatusView = {
let view = VideoUserStatusView()
return view
}()
let avatarImageView: UIImageView = {
let imageView = UIImageView(frame: .zero)
imageView.layer.masksToBounds = true
return imageView
}()
private var isViewReady = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else {
return
}
isViewReady = true
constructViewHierarchy()
activateConstraints()
contentView.backgroundColor = .clear
}
private func constructViewHierarchy() {
scrollRenderView.addSubview(renderView)
scrollRenderView.addSubview(backgroundMaskView)
contentView.addSubview(scrollRenderView)
contentView.addSubview(avatarImageView)
contentView.addSubview(userInfoView)
}
private func activateConstraints() {
scrollRenderView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(2)
}
renderView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.equalToSuperview()
make.height.equalToSuperview()
}
backgroundMaskView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
userInfoView.snp.makeConstraints { make in
make.height.equalTo(24)
make.bottom.equalToSuperview().offset(-5)
make.leading.equalToSuperview().offset(5)
make.width.lessThanOrEqualTo(self).multipliedBy(0.9)
}
}
func reset() {
videoItem = nil
cancellableSet.removeAll()
resetBorderColor()
}
override func prepareForReuse() {
super.prepareForReuse()
reset()
scrollRenderView.zoomScale = 1.0
}
deinit {
NSObject.cancelPreviousPerformRequests(withTarget: self)
debugPrint("deinit \(self)")
}
}
extension MultiStreamCell: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return isSupportedAmplification ? renderView : nil
}
}
// MARK: - Public
extension MultiStreamCell {
func updateUI(item: UserInfo) {
videoItem = item
let placeholder = UIImage(named: "room_default_user", in: tuiRoomKitBundle(), compatibleWith: nil)
avatarImageView.sd_setImage(with: URL(string: item.avatarUrl), placeholderImage: placeholder)
avatarImageView.isHidden = item.videoStreamType == .screenStream ? true : item.hasVideoStream
backgroundMaskView.isHidden = item.videoStreamType == .screenStream ? true : item.hasVideoStream
userInfoView.updateUserStatus(item)
scrollRenderView.layer.borderColor = UIColor.clear.cgColor
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
guard let self = self else { return }
let width = min(self.mm_w / 2, 72)
self.avatarImageView.layer.cornerRadius = width * 0.5
guard let _ = self.avatarImageView.superview else { return }
self.avatarImageView.snp.remakeConstraints { make in
make.height.width.equalTo(width)
make.center.equalToSuperview()
}
}
}
func updateUIVolume(item: UserInfo) {
guard videoItem?.userId == item.userId else { return }
videoItem?.hasAudioStream = item.hasAudioStream
userInfoView.updateUserVolume(hasAudio: item.hasAudioStream, volume: item.userVoiceVolume)
lastVolumeUpdateTime = Date().timeIntervalSince1970
if item.userVoiceVolume > 0 && item.hasAudioStream {
if item.videoStreamType != .screenStream {
if !isBorderHighlighted {
scrollRenderView.layer.borderColor = UIColor(0xA5FE33).cgColor
isBorderHighlighted = true
}
scheduleBorderReset()
}
} else {
resetBorderColor()
}
}
private func scheduleBorderReset() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
guard let self = self else { return }
let now = Date().timeIntervalSince1970
if now - self.lastVolumeUpdateTime >= 2 {
self.resetBorderColor()
}
}
}
private func resetBorderColor() {
scrollRenderView.layer.borderColor = UIColor.clear.cgColor
isBorderHighlighted = false
}
}
class VideoUserStatusView: UIView {
private var isShownHomeOwnerImageView: Bool = false
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else {
return
}
isViewReady = true
constructViewHierarchy()
activateConstraints()
backgroundColor = UIColor(0x22262E, alpha: 0.8)
layer.cornerRadius = 12
layer.masksToBounds = true
}
private func constructViewHierarchy() {
addSubview(homeOwnerImageView)
addSubview(voiceVolumeImageView)
addSubview(userNameLabel)
}
private let homeOwnerImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "room_homeowner", in: tuiRoomKitBundle(), compatibleWith: nil))
imageView.layer.cornerRadius = 12
imageView.layer.masksToBounds = true
return imageView
}()
private let userNameLabel: UILabel = {
let user = UILabel()
user.textColor = .white
user.backgroundColor = UIColor.clear
user.textAlignment = isRTL ? .right : .left
user.numberOfLines = 1
user.font = UIFont(name: "PingFangSC-Regular", size: 12)
return user
}()
private let voiceVolumeImageView: VolumeView = {
let imageView = VolumeView()
return imageView
}()
private func activateConstraints() {
updateOwnerImageConstraints()
voiceVolumeImageView.snp.remakeConstraints { make in
make.leading.equalTo(homeOwnerImageView.snp.trailing).offset(6.scale375())
make.width.height.equalTo(14)
make.centerY.equalToSuperview()
}
userNameLabel.snp.makeConstraints { make in
make.leading.equalTo(voiceVolumeImageView.snp.trailing).offset(5)
make.centerY.equalToSuperview()
make.trailing.equalToSuperview().offset(-8)
}
}
private func updateOwnerImageConstraints() {
guard let _ = homeOwnerImageView.superview else { return }
homeOwnerImageView.snp.remakeConstraints { make in
make.leading.equalToSuperview()
make.width.height.equalTo(isShownHomeOwnerImageView ? 24 : 0)
make.top.bottom.equalToSuperview()
}
}
}
// MARK: - Public
extension VideoUserStatusView {
func updateUserStatus(_ item: UserInfo) {
if !item.userName.isEmpty {
userNameLabel.text = item.userName
} else {
userNameLabel.text = item.userId
}
if item.userRole == .roomOwner {
homeOwnerImageView.image = UIImage(named: "room_homeowner", in: tuiRoomKitBundle(), compatibleWith: nil)
} else if item.userRole == .administrator {
homeOwnerImageView.image = UIImage(named: "room_administrator", in: tuiRoomKitBundle(), compatibleWith: nil)
}
isShownHomeOwnerImageView = item.userRole != .generalUser
homeOwnerImageView.isHidden = !isShownHomeOwnerImageView
updateOwnerImageConstraints()
updateUserVolume(hasAudio: item.hasAudioStream, volume: item.userVoiceVolume)
}
func updateUserVolume(hasAudio: Bool, volume: Int) {
voiceVolumeImageView.updateVolume(CGFloat(volume))
voiceVolumeImageView.updateAudio(hasAudio)
}
}
//
// MultiStreamsSorter.swift
// TUIRoomKit
//
// Created by CY zhao on 2024/12/18.
//
import Foundation
class MultiStreamsSorter {
private let currentUserId: String
private var roomOwnerId: String = ""
init(currentUserId: String) {
self.currentUserId = currentUserId
}
func sortStreams(_ videoItems: [UserInfo]) -> [UserInfo] {
guard needSorting(videoItems) else { return videoItems }
var sortedItems = videoItems
if let ownerIndex = sortedItems.firstIndex(where: { $0.userRole == .roomOwner }) {
let owner = sortedItems.remove(at: ownerIndex)
roomOwnerId = owner.userId
sortedItems.insert(owner, at: 0)
}
if currentUserId != roomOwnerId,
let currentUserIndex = sortedItems.firstIndex(where: { $0.userId == currentUserId }) {
let currentUser = sortedItems.remove(at: currentUserIndex)
sortedItems.insert(currentUser, at: 1)
}
let sortedOthers = sortedItems
.dropFirst(currentUserId == roomOwnerId ? 1 : 2)
.sorted { first, second in
let firstStatus = StreamStatus(hasAudio: first.hasAudioStream,
hasVideo: first.hasVideoStream)
let secondStatus = StreamStatus(hasAudio: second.hasAudioStream,
hasVideo: second.hasVideoStream)
return firstStatus.priority < secondStatus.priority
}
return Array(sortedItems.prefix(currentUserId == roomOwnerId ? 1 : 2)) + sortedOthers
}
private func needSorting(_ videoItems: [UserInfo]) -> Bool {
guard videoItems.count > 1 else { return false }
if let firstUser = videoItems.first,
firstUser.userRole != .roomOwner,
videoItems.contains(where: { $0.userRole == .roomOwner}) {
return true
}
if currentUserId != roomOwnerId {
let currentUserExpectedPosition = 1
if videoItems.count > currentUserExpectedPosition,
videoItems[currentUserExpectedPosition].userId != currentUserId,
videoItems.contains(where: { $0.userId == currentUserId }) {
return true
}
}
let startIndex = currentUserId == roomOwnerId ? 1 : 2
guard videoItems.count > startIndex + 1 else { return false }
let otherStreams = Array(videoItems[startIndex...])
for i in 0..<(otherStreams.count - 1) {
let current = otherStreams[i]
let next = otherStreams[i + 1]
let currentStreamState = StreamStatus(hasAudio: current.hasAudioStream, hasVideo: current.hasVideoStream)
let nexdtStreamState = StreamStatus(hasAudio: next.hasAudioStream, hasVideo: next.hasVideoStream)
if currentStreamState.priority > nexdtStreamState.priority {
return true
}
}
return false
}
}
enum StreamStatus {
case videoAndAudio
case videoOnly
case audioOnly
case none
init(hasAudio: Bool, hasVideo: Bool) {
switch(hasAudio, hasVideo) {
case(true, true): self = .videoAndAudio
case(false, true): self = .videoOnly
case(true, false): self = .audioOnly
case(false, false): self = .none
}
}
var priority: Int {
switch self {
case .videoAndAudio: return 0
case .videoOnly: return 1
case .audioOnly: return 2
case .none: return 3
}
}
}
//
// VideoStoreRegister.swift
// TUIRoomKit
//
// Created by CY zhao on 2024/11/7.
//
import Foundation
import Factory
extension Container {
var videoStore: Factory<VideoStore> {
self { VideoStoreProvider() }.shared
}
}
// //
// CGFloat+Extension.swift // CGFloat+Extension.swift
// Alamofire
// //
// Created by aby on 2022/12/26. // Created by aby on 2022/12/26.
// Copyright © 2022 Tencent. All rights reserved. // Copyright © 2022 Tencent. All rights reserved.
......
//
// BellFeature.swift
// Pods
//
// Created by janejntang on 2024/11/20.
//
import AVFAudio
class BellFeature: NSObject, AVAudioPlayerDelegate {
private var audioPlayer: AVAudioPlayer?
private let placeholderBellName = "phone_ringing"
private let placeholderBellType = "mp3"
private lazy var customBellPath: String? = {
return ConferenceSession.sharedInstance.implementation.bellPath
}()
private lazy var enableMuteMode: Bool = {
return ConferenceSession.sharedInstance.implementation.enableMuteMode
}()
private var needPlayRingtone = false
override init() {
super.init()
registerNotifications()
}
private func registerNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(handleInterruption(_:)), name: AVAudioSession.interruptionNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleAppWillResignActive), name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleAppDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc func handleInterruption(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
guard let userInfo = notification.userInfo else { return }
guard let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt else { return }
guard let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }
switch type {
case .began:
self.audioPlayer?.pause()
case .ended:
let optinsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt ?? 0
let options = AVAudioSession.InterruptionOptions(rawValue: optinsValue)
if options.contains(.shouldResume), !self.enableMuteMode, let audioPlayer = self.audioPlayer {
audioPlayer.play()
}
default: break
}
}
}
@objc func handleAppWillResignActive() {
audioPlayer?.pause()
}
@objc func handleAppDidBecomeActive() {
if needPlayRingtone {
playBell()
needPlayRingtone = false
} else if let audioPlayer = audioPlayer, !audioPlayer.isPlaying, !self.enableMuteMode {
audioPlayer.play()
}
}
func playBell() {
guard !enableMuteMode else { return }
guard let bellURL = findBellPath() else { return }
if UIApplication.shared.applicationState != .background {
playAudio(bellURL)
} else {
needPlayRingtone = true
}
}
func stopBell() {
audioPlayer?.stop()
audioPlayer = nil
needPlayRingtone = false
}
private func findBellPath() -> URL? {
let customBellPath = customBellPath?.replacingOccurrences(of: "file://", with: "")
if let bellPath = customBellPath, checkResourceExisted(path: bellPath) {
return URL(fileURLWithPath: bellPath)
} else {
return tuiRoomKitBundle().url(forResource: placeholderBellName, withExtension: placeholderBellType)
}
}
private func checkResourceExisted(path: String) -> Bool {
return FileManager.default.fileExists(atPath: path)
}
private func playAudio(_ audioURL: URL) {
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
} catch let error {
debugPrint("AVAudioSession set outputAudioPort error:\(error.localizedDescription)")
}
do {
try audioPlayer = AVAudioPlayer(contentsOf: audioURL)
audioPlayer?.numberOfLoops = -1
audioPlayer?.delegate = self
audioPlayer?.prepareToPlay()
} catch let error {
debugPrint("audioPlayer error: \(error.localizedDescription)")
}
audioPlayer?.play()
}
}
//
// vibrationFeature.swift
// Pods
//
// Created by janejntang on 2024/11/20.
//
import AVFAudio
class VibrationFeature {
private var vibrationTimer: Timer?
private let vibrationInterval = 0.1
private let maxVibrationTime = 60.0
private lazy var enableVibrationMode: Bool = {
return ConferenceSession.sharedInstance.implementation.enableVibrationMode
}()
func playVibrate() {
guard enableVibrationMode else { return }
var vibrationCount = 0
let vibrationTotalCount = Int(maxVibrationTime / vibrationInterval)
vibrationTimer = Timer.scheduledTimer(withTimeInterval: vibrationInterval, repeats: true) { [weak self] timer in
guard let self = self else { return }
guard UIApplication.shared.applicationState == .active else { return }
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
vibrationCount = vibrationCount + 1
if vibrationCount >= vibrationTotalCount {
self.stopVibrate()
}
}
}
func stopVibrate() {
vibrationTimer?.invalidate()
vibrationTimer = nil
}
deinit {
debugPrint("deinit:\(self)")
}
}
//
// RoomKitLog.swift
// TUIRoomKit
//
// Created by CY Zhao on 2024/12/12.
//
import Foundation
#if canImport(TXLiteAVSDK_TRTC)
import TXLiteAVSDK_TRTC
#elseif canImport(TXLiteAVSDK_Professional)
import TXLiteAVSDK_Professional
#endif
class RoomKitLog {
private static let API = "TuikitLog"
private static let LOG_KEY_API = "api"
private static let LOG_KEY_PARAMS = "params"
private static let LOG_KEY_PARAMS_LEVEL = "level"
private static let LOG_KEY_PARAMS_MESSAGE = "message"
private static let LOG_KEY_PARAMS_FILE = "file"
private static let LOG_KEY_PARAMS_LINE = "line"
private static let LOG_KEY_PARAMS_MODULE = "module"
private static let LOG_KEY_PARAMS_MODULE_VALUE = "TUIRoomKit"
private func `init`() {}
enum RoomKitLogLevel: Int {
case error = 2
case warn = 1
case info = 0
}
static func error(_ file: String, _ line: String, _ messages: String...) {
log(level: .error, file: file, line: line, messages)
}
static func warn(_ file: String, _ line: String, _ messages: String...) {
log(level: .warn, file: file, line: line, messages)
}
static func info(_ file: String, _ line: String, _ messages: String...) {
log(level: .info, file: file, line: line, messages)
}
private static func log(level: RoomKitLogLevel = .info, file: String, line: String, _ messages: [String]) {
let apiParams: [String: Any] = [
LOG_KEY_API: API,
LOG_KEY_PARAMS: [
LOG_KEY_PARAMS_LEVEL: level.rawValue,
LOG_KEY_PARAMS_MESSAGE: messages.joined(),
LOG_KEY_PARAMS_MODULE: LOG_KEY_PARAMS_MODULE_VALUE,
LOG_KEY_PARAMS_FILE: file,
LOG_KEY_PARAMS_LINE: Int(line) ?? 0,
],
]
let jsonData = try? JSONSerialization.data(withJSONObject: apiParams, options: .prettyPrinted)
guard let jsonData = jsonData, let jsonString = String(data: jsonData, encoding: .utf8) else { return }
TRTCCloud.sharedInstance().callExperimentalAPI(jsonString)
}
}
//
// RoomKitReport.swift
// Pods
//
// Created by janejntang on 2025/1/17.
//
import RTCRoomEngine
enum DataReport: Int {
case metricsBarragePanelShow = 106061
case metricsBarrageSendMessage = 106062
case metricsFloatWindowShow = 106063
case metricsUserListPanelShow = 106064
case metricsUserListSearch = 106065
case metricsSettingsPanelShow = 106066
case metricsChatPanelShow = 106057
case metricsShareRoomInfoPanelShow = 106067
case metricsConferenceSchedulePanelShow = 106068
case metricsConferenceModifyPanelShow = 106069
case metricsConferenceInfoPanelShow = 106070
case metricsConferenceAttendee = 106071
case metricsWaterMarkEnable = 106054
case metricsWaterMarkCustomText = 106060
}
class RoomKitReport {
private static let REPORT_API_KEY = "api"
private static let REPORT_API_VALUE = "KeyMetricsStats"
private static let REPORT_PARAMS = "params"
private static let REPORT_PARAMS_KEY = "key"
private func `init`() {}
static func reportData(_ dataReport: DataReport) {
let params: [String: Any] = [
REPORT_API_KEY: REPORT_API_VALUE,
REPORT_PARAMS: [
REPORT_PARAMS_KEY: dataReport.rawValue
]
]
guard let jsonString = params.convertToString() else { return }
TUIRoomEngine.callExperimentalAPI(jsonStr: jsonString)
}
}
...@@ -11,7 +11,7 @@ import Factory ...@@ -11,7 +11,7 @@ import Factory
enum ConferenceRoute { enum ConferenceRoute {
case none case none
case schedule(memberSelectFactory: MemberSelectionFactory?) case schedule
case main(conferenceParams: ConferenceParamType) case main(conferenceParams: ConferenceParamType)
case selectMember(memberSelectParams: MemberSelectParams?) case selectMember(memberSelectParams: MemberSelectParams?)
case selectedMember(showDeleteButton: Bool, selectedMembers: [UserInfo]) case selectedMember(showDeleteButton: Bool, selectedMembers: [UserInfo])
...@@ -20,7 +20,7 @@ enum ConferenceRoute { ...@@ -20,7 +20,7 @@ enum ConferenceRoute {
case modifySchedule(conferenceInfo: ConferenceInfo) case modifySchedule(conferenceInfo: ConferenceInfo)
case popup(view: UIView) case popup(view: UIView)
case alert(state: AlertState) case alert(state: AlertState)
case invitation(roomInfo: TUIRoomInfo, invitation: TUIInvitation) case invitation(roomInfo: RoomInfo, invitation: TUIInvitation)
func hideNavigationBar() -> Bool { func hideNavigationBar() -> Bool {
switch self { switch self {
...@@ -43,11 +43,7 @@ enum ConferenceRoute { ...@@ -43,11 +43,7 @@ enum ConferenceRoute {
self = .none self = .none
} }
case is ScheduleConferenceViewController: case is ScheduleConferenceViewController:
guard let vc = viewController as? ScheduleConferenceViewController else { self = .schedule
self = .none
break
}
self = .schedule(memberSelectFactory: vc.memberSelectionFactory)
case _ as ContactViewProtocol: case _ as ContactViewProtocol:
self = .selectMember(memberSelectParams: nil) self = .selectMember(memberSelectParams: nil)
case is SelectedMembersViewController: case is SelectedMembersViewController:
...@@ -65,7 +61,7 @@ enum ConferenceRoute { ...@@ -65,7 +61,7 @@ enum ConferenceRoute {
self = .modifySchedule(conferenceInfo: vc?.conferenceInfo ?? ConferenceInfo()) self = .modifySchedule(conferenceInfo: vc?.conferenceInfo ?? ConferenceInfo())
case is ConferenceInvitationViewController: case is ConferenceInvitationViewController:
let vc = viewController as? ConferenceInvitationViewController let vc = viewController as? ConferenceInvitationViewController
self = .invitation(roomInfo: vc?.roomInfo ?? TUIRoomInfo(), invitation: vc?.invitation ?? TUIInvitation()) self = .invitation(roomInfo: vc?.roomInfo ?? RoomInfo(), invitation: vc?.invitation ?? TUIInvitation())
case is PopupViewController: case is PopupViewController:
let vc = viewController as? PopupViewController let vc = viewController as? PopupViewController
self = .popup(view: vc?.contentView ?? viewController.view) self = .popup(view: vc?.contentView ?? viewController.view)
...@@ -90,8 +86,8 @@ extension ConferenceRoute { ...@@ -90,8 +86,8 @@ extension ConferenceRoute {
vc.setJoinConferenceParams(params: joinConferenceParams) vc.setJoinConferenceParams(params: joinConferenceParams)
} }
return vc return vc
case .schedule(memberSelectFactory: let factory): case .schedule:
return ScheduleConferenceViewController(memberSelectFactory: factory) return ScheduleConferenceViewController()
case .selectMember(memberSelectParams: let memberSelectParams): case .selectMember(memberSelectParams: let memberSelectParams):
guard let params = memberSelectParams else { guard let params = memberSelectParams else {
return UIViewController() return UIViewController()
...@@ -131,10 +127,11 @@ extension ConferenceRoute { ...@@ -131,10 +127,11 @@ extension ConferenceRoute {
} }
func getSelectMemberViewController(participants: ConferenceParticipants) -> (ContactViewProtocol & UIViewController)? { func getSelectMemberViewController(participants: ConferenceParticipants) -> (ContactViewProtocol & UIViewController)? {
guard let vc = Container.shared.contactViewController(participants) as? (ContactViewProtocol & UIViewController) else { if ConferenceSession.sharedInstance.implementation.hasCustomContacts {
return nil return Container.shared.contactViewController.resolve(participants) as? (ContactViewProtocol & UIViewController)
} else {
return SelectMemberViewController(participants: participants)
} }
return vc
} }
} }
......
...@@ -18,13 +18,11 @@ typealias ConferenceNavigation = NavigationAction<ConferenceRoute> ...@@ -18,13 +18,11 @@ typealias ConferenceNavigation = NavigationAction<ConferenceRoute>
struct ConferenceRouteState { struct ConferenceRouteState {
var currentRouteAction: ConferenceNavigation = .presented(route: .none) var currentRouteAction: ConferenceNavigation = .presented(route: .none)
var currentRoute: ConferenceRoute = .none var currentRoute: ConferenceRoute = .none
var memberSelectionFactory: MemberSelectionFactory?
} }
enum ConferenceNavigationAction { enum ConferenceNavigationAction {
static let key = "conference.navigation.action" static let key = "conference.navigation.action"
static let navigate = ActionTemplate(id: key.appending("navigate"), payloadType: ConferenceNavigation.self) static let navigate = ActionTemplate(id: key.appending("navigate"), payloadType: ConferenceNavigation.self)
static let setMemberSelectionFactory = ActionTemplate(id: key.appending(".setMemberSelectionFactory"), payloadType: MemberSelectionFactory.self)
} }
let routeReducer = Reducer<ConferenceRouteState>( let routeReducer = Reducer<ConferenceRouteState>(
...@@ -36,8 +34,5 @@ let routeReducer = Reducer<ConferenceRouteState>( ...@@ -36,8 +34,5 @@ let routeReducer = Reducer<ConferenceRouteState>(
default: default:
break break
} }
}),
ReduceOn(ConferenceNavigationAction.setMemberSelectionFactory, reduce: { state, action in
state.memberSelectionFactory = action.payload
}) })
) )
...@@ -20,7 +20,6 @@ protocol Route: ActionDispatcher { ...@@ -20,7 +20,6 @@ protocol Route: ActionDispatcher {
} }
private let currentRouteActionSelector = Selector(keyPath: \ConferenceRouteState.currentRouteAction) private let currentRouteActionSelector = Selector(keyPath: \ConferenceRouteState.currentRouteAction)
private let memberSelectFactorySelector = Selector(keyPath: \ConferenceRouteState.memberSelectionFactory)
class ConferenceRouter: NSObject { class ConferenceRouter: NSObject {
override init() { override init() {
...@@ -116,9 +115,9 @@ extension ConferenceRouter: Route { ...@@ -116,9 +115,9 @@ extension ConferenceRouter: Route {
} }
func showContactView(delegate: ContactViewSelectDelegate, participants: ConferenceParticipants) { func showContactView(delegate: ContactViewSelectDelegate, participants: ConferenceParticipants) {
guard let factory = store.selectCurrent(memberSelectFactorySelector) else { return } let selectParams = MemberSelectParams(participants: participants, delegate: delegate)
let selectParams = MemberSelectParams(participants: participants, delegate: delegate, factory: factory)
store.dispatch(action: ConferenceNavigationAction.navigate(payload: .push(route: .selectMember(memberSelectParams: selectParams)))) store.dispatch(action: ConferenceNavigationAction.navigate(payload: .push(route: .selectMember(memberSelectParams: selectParams))))
RoomKitReport.reportData(.metricsConferenceAttendee)
} }
func dispatch(action: Action) { func dispatch(action: Action) {
......
...@@ -64,11 +64,12 @@ class ConferenceOptions { ...@@ -64,11 +64,12 @@ class ConferenceOptions {
private static func createRoomInfo(startConferenceParams: StartConferenceParams) -> TUIRoomInfo { private static func createRoomInfo(startConferenceParams: StartConferenceParams) -> TUIRoomInfo {
let roomInfo = TUIRoomInfo() let roomInfo = TUIRoomInfo()
roomInfo.roomId = startConferenceParams.roomId roomInfo.roomId = startConferenceParams.roomId
roomInfo.isMicrophoneDisableForAllUser = !startConferenceParams.isOpenMicrophone roomInfo.isMicrophoneDisableForAllUser = startConferenceParams.isMicrophoneDisableForAllUser
roomInfo.isCameraDisableForAllUser = !startConferenceParams.isOpenCamera roomInfo.isCameraDisableForAllUser = startConferenceParams.isCameraDisableForAllUser
roomInfo.isSeatEnabled = startConferenceParams.isSeatEnabled roomInfo.isSeatEnabled = startConferenceParams.isSeatEnabled
roomInfo.name = startConferenceParams.name ?? "" roomInfo.name = startConferenceParams.name ?? ""
roomInfo.seatMode = .applyToTake roomInfo.seatMode = .applyToTake
roomInfo.password = startConferenceParams.password ?? ""
return roomInfo return roomInfo
} }
......
...@@ -20,6 +20,7 @@ import RTCRoomEngine ...@@ -20,6 +20,7 @@ import RTCRoomEngine
public var isSeatEnabled = false public var isSeatEnabled = false
public var name: String? public var name: String?
public var password: String?
public init(roomId: String, public init(roomId: String,
isOpenMicrophone: Bool = true, isOpenMicrophone: Bool = true,
...@@ -28,7 +29,8 @@ import RTCRoomEngine ...@@ -28,7 +29,8 @@ import RTCRoomEngine
isMicrophoneDisableForAllUser: Bool = false, isMicrophoneDisableForAllUser: Bool = false,
isCameraDisableForAllUser: Bool = false, isCameraDisableForAllUser: Bool = false,
isSeatEnabled: Bool = false, isSeatEnabled: Bool = false,
name: String? = nil) { name: String? = nil,
password: String? = nil) {
self.roomId = roomId self.roomId = roomId
self.isOpenMicrophone = isOpenMicrophone self.isOpenMicrophone = isOpenMicrophone
self.isOpenCamera = isOpenCamera self.isOpenCamera = isOpenCamera
...@@ -37,6 +39,7 @@ import RTCRoomEngine ...@@ -37,6 +39,7 @@ import RTCRoomEngine
self.isCameraDisableForAllUser = isCameraDisableForAllUser self.isCameraDisableForAllUser = isCameraDisableForAllUser
self.isSeatEnabled = isSeatEnabled self.isSeatEnabled = isSeatEnabled
self.name = name self.name = name
self.password = password
super.init() super.init()
} }
} }
...@@ -65,6 +68,22 @@ import RTCRoomEngine ...@@ -65,6 +68,22 @@ import RTCRoomEngine
@objc public protocol ConferenceObserver { @objc public protocol ConferenceObserver {
@objc optional func onConferenceStarted(roomInfo: TUIRoomInfo, error: TUIError, message: String) @objc optional func onConferenceStarted(roomInfo: TUIRoomInfo, error: TUIError, message: String)
@objc optional func onConferenceJoined(roomInfo: TUIRoomInfo, error: TUIError, message: String) @objc optional func onConferenceJoined(roomInfo: TUIRoomInfo, error: TUIError, message: String)
@objc optional func onConferenceFinished(roomId: String) @objc optional func onConferenceFinished(roomInfo: TUIRoomInfo, reason: ConferenceFinishedReason)
@objc optional func onConferenceExited(roomId: String) @objc optional func onConferenceExited(roomInfo: TUIRoomInfo, reason: ConferenceExitedReason)
}
@objc
public enum ConferenceFinishedReason: Int {
case finishedByOwner
case finishedByServer
}
@objc
public enum ConferenceExitedReason: Int {
case exitedBySelf
case exitedByAdminKickOut
case exitedByServerKickOut
case exitedByJoinedOnOtherDevice
case exitedByKickedOutOfLine
case exitedByUserSigExpired
} }
...@@ -18,13 +18,10 @@ struct ConferenceSection { ...@@ -18,13 +18,10 @@ struct ConferenceSection {
@objcMembers public class ConferenceListView: UIView { @objcMembers public class ConferenceListView: UIView {
// MARK: - Intailizer // MARK: - Intailizer
public init(viewController: UIViewController, memberSelectFactory: MemberSelectionFactory?) { public init(viewController: UIViewController) {
super.init(frame: .zero) super.init(frame: .zero)
let viewRoute = ConferenceRoute.init(viewController: viewController) let viewRoute = ConferenceRoute.init(viewController: viewController)
navigation.initializeRoute(viewController: viewController, rootRoute: viewRoute) navigation.initializeRoute(viewController: viewController, rootRoute: viewRoute)
if let factory = memberSelectFactory {
navigation.dispatch(action: ConferenceNavigationAction.setMemberSelectionFactory(payload: factory))
}
} }
@available(*, unavailable, message: "Use init(viewController:) instead") @available(*, unavailable, message: "Use init(viewController:) instead")
...@@ -160,6 +157,7 @@ struct ConferenceSection { ...@@ -160,6 +157,7 @@ struct ConferenceSection {
private func bindInteraction() { private func bindInteraction() {
subscribeToast() subscribeToast()
subscribeScheduleSubject() subscribeScheduleSubject()
subscribeRoomSubject()
store.dispatch(action: ConferenceListActions.fetchConferenceList(payload: (fetchListCursor, conferencesPerFetch))) store.dispatch(action: ConferenceListActions.fetchConferenceList(payload: (fetchListCursor, conferencesPerFetch)))
conferenceListPublisher conferenceListPublisher
.receive(on: DispatchQueue.global(qos: .default)) .receive(on: DispatchQueue.global(qos: .default))
...@@ -345,6 +343,18 @@ extension ConferenceListView { ...@@ -345,6 +343,18 @@ extension ConferenceListView {
} }
.store(in: &cancellableSet) .store(in: &cancellableSet)
} }
private func subscribeRoomSubject() {
store.roomActionSubject
.receive(on: RunLoop.main)
.filter { $0.id == RoomResponseActions.onExitSuccess.id }
.sink { [weak self] action in
guard let self = self else { return }
self.store.dispatch(action: ScheduleViewActions.refreshConferenceList())
}
.store(in: &cancellableSet)
}
} }
private extension String { private extension String {
......
...@@ -29,12 +29,7 @@ import TUICore ...@@ -29,12 +29,7 @@ import TUICore
super.viewDidLoad() super.viewDidLoad()
RoomRouter.shared.initializeNavigationController(rootViewController: self) RoomRouter.shared.initializeNavigationController(rootViewController: self)
RoomVideoFloatView.dismiss() RoomVideoFloatView.dismiss()
#if RTCube_APPSTORE
let selector = NSSelectorFromString("showAlertUserLiveTips")
if responds(to: selector) {
perform(selector)
}
#endif
viewModel.onViewDidLoadAction() viewModel.onViewDidLoadAction()
subscribeToast() subscribeToast()
} }
......
...@@ -35,4 +35,23 @@ import Foundation ...@@ -35,4 +35,23 @@ import Foundation
implementation.setContactsViewProvider(provider) implementation.setContactsViewProvider(provider)
} }
@objc public func setCallingBell(filePath: String){
implementation.setCallingBell(filePath: filePath)
}
@objc public func enableMuteMode(enable: Bool) {
implementation.enableMuteMode(enable: enable)
}
@objc public func enableVibrationMode(enable: Bool) {
implementation.enableVibrationMode(enable: enable)
}
@objc public func setAppGroup(_ appGroup: String) {
implementation.setAppGroup(appGroup)
}
@objc public func setParticipants(_ participants: [User]) {
implementation.setParticipants(participants)
}
} }
...@@ -11,11 +11,8 @@ import RTCRoomEngine ...@@ -11,11 +11,8 @@ import RTCRoomEngine
import Combine import Combine
import TUICore import TUICore
public typealias MemberSelectionFactory = ([User]) -> ContactViewProtocol
@objcMembers public class ScheduleConferenceViewController: UIViewController { @objcMembers public class ScheduleConferenceViewController: UIViewController {
private var cancellableSet = Set<AnyCancellable>() private var cancellableSet = Set<AnyCancellable>()
let memberSelectionFactory: MemberSelectionFactory?
private let durationTime = 1800 private let durationTime = 1800
private let reminderSecondsBeforeStart = 600 private let reminderSecondsBeforeStart = 600
lazy var rootView: ScheduleConferenceTableView = { lazy var rootView: ScheduleConferenceTableView = {
...@@ -30,18 +27,6 @@ public typealias MemberSelectionFactory = ([User]) -> ContactViewProtocol ...@@ -30,18 +27,6 @@ public typealias MemberSelectionFactory = ([User]) -> ContactViewProtocol
return .portrait return .portrait
} }
public init(memberSelectFactory: MemberSelectionFactory?) {
self.memberSelectionFactory = memberSelectFactory
super.init(nibName: nil, bundle: nil)
if let factory = memberSelectFactory {
route.dispatch(action: ConferenceNavigationAction.setMemberSelectionFactory(payload: factory))
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func loadView() { public override func loadView() {
self.view = rootView self.view = rootView
} }
...@@ -51,6 +36,7 @@ public typealias MemberSelectionFactory = ([User]) -> ContactViewProtocol ...@@ -51,6 +36,7 @@ public typealias MemberSelectionFactory = ([User]) -> ContactViewProtocol
initializeView() initializeView()
initializeRoute() initializeRoute()
initializeData() initializeData()
reportSchedulePanelShow()
subscribeScheduleSubject() subscribeScheduleSubject()
subscribeToast() subscribeToast()
...@@ -125,6 +111,10 @@ public typealias MemberSelectionFactory = ([User]) -> ContactViewProtocol ...@@ -125,6 +111,10 @@ public typealias MemberSelectionFactory = ([User]) -> ContactViewProtocol
return String(randomNumber) return String(randomNumber)
} }
private func reportSchedulePanelShow() {
store.reportViewShow(dataReport: .metricsConferenceSchedulePanelShow)
}
deinit { deinit {
debugPrint("deinit \(self)") debugPrint("deinit \(self)")
} }
......
//
// AudioService.swift
// Pods
//
// Created by janejntang on 2024/12/26.
//
import RTCRoomEngine
import Combine
class AudioService {
private let engine = TUIRoomEngine.sharedInstance()
private let engineManeger = EngineManager.shared //TODO: replace later
func muteLocalAudio() {
engine.muteLocalAudio()
}
func unmuteLocalAudio() -> AnyPublisher<Void, RoomError> {
return Future<Void, RoomError> { [weak self] promise in
guard let self = self else { return }
engine.unmuteLocalAudio {
promise(.success(()))
} onError: { err, message in
let error = RoomError(error: err, message: message, showToast: false)
promise(.failure(error))
}
}
.eraseToAnyPublisher()
}
func openLocalMicrophone() -> AnyPublisher<Void, RoomError> {
return Future<Void, RoomError> { [weak self] promise in
guard let self = self else { return }
engineManeger.openLocalMicrophone {
promise(.success(()))
} onError: { err, message in
let error = RoomError(error: err, message: message, showToast: false)
promise(.failure(error))
}
}
.eraseToAnyPublisher()
}
}
...@@ -84,6 +84,7 @@ extension ConferenceInvitationService: TUIConferenceInvitationObserver { ...@@ -84,6 +84,7 @@ extension ConferenceInvitationService: TUIConferenceInvitationObserver {
func onInvitationHandledByOtherDevice(roomInfo: TUIRoomInfo, accepted: Bool) { func onInvitationHandledByOtherDevice(roomInfo: TUIRoomInfo, accepted: Bool) {
guard let store = self.store else { return } guard let store = self.store else { return }
store.dispatch(action: InvitationViewActions.dismissInvitationView()) store.dispatch(action: InvitationViewActions.dismissInvitationView())
store.dispatch(action: InvitationObserverActions.stopCallingBellAndVibration())
} }
func onInvitationCancelled(roomInfo: TUIRoomInfo, invitation: TUIInvitation) { func onInvitationCancelled(roomInfo: TUIRoomInfo, invitation: TUIInvitation) {
...@@ -101,6 +102,7 @@ extension ConferenceInvitationService: TUIConferenceInvitationObserver { ...@@ -101,6 +102,7 @@ extension ConferenceInvitationService: TUIConferenceInvitationObserver {
func onInvitationTimeout(roomInfo: TUIRoomInfo, invitation: TUIInvitation) { func onInvitationTimeout(roomInfo: TUIRoomInfo, invitation: TUIInvitation) {
guard let store = self.store else { return } guard let store = self.store else { return }
store.dispatch(action: InvitationViewActions.dismissInvitationView()) store.dispatch(action: InvitationViewActions.dismissInvitationView())
store.dispatch(action: InvitationObserverActions.stopCallingBellAndVibration())
} }
func onInvitationRevokedByAdmin(roomInfo: TUIRoomInfo, invitation: TUIInvitation, admin: TUIUserInfo) { func onInvitationRevokedByAdmin(roomInfo: TUIRoomInfo, invitation: TUIInvitation, admin: TUIUserInfo) {
......
...@@ -149,6 +149,7 @@ extension ConferenceListService: TUIConferenceListManagerObserver { ...@@ -149,6 +149,7 @@ extension ConferenceListService: TUIConferenceListManagerObserver {
let currentList = store.selectCurrent(ConferenceListSelectors.getConferenceList).map { $0.basicInfo.roomId } let currentList = store.selectCurrent(ConferenceListSelectors.getConferenceList).map { $0.basicInfo.roomId }
if currentList.contains(roomId) { if currentList.contains(roomId) {
store.dispatch(action: ConferenceListActions.removeConference(payload: roomId)) store.dispatch(action: ConferenceListActions.removeConference(payload: roomId))
store.dispatch(action: ScheduleResponseActions.onConferenceRemoved(payload: roomId))
} }
} }
......
...@@ -21,3 +21,56 @@ struct RoomError: Error { ...@@ -21,3 +21,56 @@ struct RoomError: Error {
} }
} }
} }
protocol LocalizedError {
var description: String?{get}
var isCommon: Bool{get}
}
extension TUIError: LocalizedError {
var description: String? {
switch self {
case .roomIdNotExist:
return .roomIdNotExist
case .roomIdOccupied:
return .roomIdOccupied
case .roomUserFull:
return .roomUserFull
case .roomNameInvalid:
return .roomNameInvalid
case .roomIdInvalid:
return .roomIdInvalid
case .operationInvalidBeforeEnterRoom:
return .operationInvalidBeforeEnterRoom
case .operationNotSupportedInCurrentRoomType:
return .operationNotSupportedInCurrentRoomType
case .alreadyInOtherRoom:
return .alreadyInOtherRoom
default:
return nil
}
}
var isCommon: Bool {
switch self {
case .roomIdNotExist, .roomIdOccupied, .roomUserFull:
return true
default:
return false
}
}
}
private extension String {
static let roomIdNotExist = localized("The room does not exist, please confirm the room ID or create a room!")
static let operationInvalidBeforeEnterRoom = localized("You need to enter the room to use this function.")
static let operationNotSupportedInCurrentRoomType = localized("This operation is not supported in the current room type.")
static let roomIdInvalid = localized("The room number is invalid. It must be printable ASCII characters and cannot exceed 48 bytes.")
static let roomIdOccupied = localized("The room ID is occupied, please select another room ID.")
static let roomNameInvalid = localized("The room name is invalid. It cannot exceed 30 bytes. If it contains Chinese characters, the character encoding must be UTF-8.")
static let alreadyInOtherRoom = localized("You are already in another room and need to leave the room before joining a new room.")
static let roomUserFull = localized("The room is full and you cannot enter the room temporarily.")
}
...@@ -8,15 +8,40 @@ ...@@ -8,15 +8,40 @@
import Foundation import Foundation
import RTCRoomEngine import RTCRoomEngine
import Factory import Factory
import TUICore
import Combine
class InvitationObserverService: NSObject { public class InvitationObserverService: NSObject {
static let shared = InvitationObserverService() @objc public static let shared = InvitationObserverService()
private var invitationWindow: UIWindow? private var invitationWindow: UIWindow?
private var getInvitationListCursor: String = ""
private let singleFetchCount: Int = 20
private let bellFeature = BellFeature()
private let vibrationFeature = VibrationFeature()
private override init() { private override init() {
} }
func showInvitationWindow(roomInfo: TUIRoomInfo, invitation: TUIInvitation) { @objc public func show(extString: String) {
let store = Container.shared.conferenceStore()
let isEnteredRoom = store.selectCurrent(RoomSelectors.getIsEnteredRoom)
if isEnteredRoom || invitationWindow != nil {
return
}
guard let dict = extString.convertToDic() else { return }
guard let roomId = dict["RoomId"] as? String else { return }
guard let roomName = dict["RoomName"] as? String else { return }
guard let ownerName = dict["OwnerName"] as? String else { return }
guard let memberCount = dict["MemberCount"] as? Int else { return }
var roomInfo = RoomInfo()
roomInfo.roomId = roomId
roomInfo.name = roomName
roomInfo.ownerName = ownerName
roomInfo.memberCount = memberCount
self.getInvitationList(roomInfo: roomInfo)
}
func showInvitationWindow(roomInfo: RoomInfo, invitation: TUIInvitation) {
DispatchQueue.main.async { DispatchQueue.main.async {
let invitationViewController = ConferenceInvitationViewController(roomInfo: roomInfo, invitation: invitation) let invitationViewController = ConferenceInvitationViewController(roomInfo: roomInfo, invitation: invitation)
self.invitationWindow = UIWindow() self.invitationWindow = UIWindow()
...@@ -33,39 +58,72 @@ class InvitationObserverService: NSObject { ...@@ -33,39 +58,72 @@ class InvitationObserverService: NSObject {
self.invitationWindow = nil self.invitationWindow = nil
} }
} }
func playCallingBellAndVibration() {
bellFeature.playBell()
vibrationFeature.playVibrate()
}
func stopCallingBellAndVibration() {
bellFeature.stopBell()
vibrationFeature.stopVibrate()
}
private func getInvitationList(roomInfo: RoomInfo) {
let invitationManager = TUIRoomEngine.sharedInstance().getExtension(extensionType: .conferenceInvitationManager) as? TUIConferenceInvitationManager
let selfUserId = TUILogin.getUserID()
invitationManager?.getInvitationList(roomInfo.roomId, cursor: getInvitationListCursor, count: singleFetchCount) { [weak self] invitations, cursor in
guard let self = self else { return }
for invitation in invitations {
if invitation.invitee.userId != selfUserId {
continue
}
if invitation.status == .pending {
self.showInvitationWindow(roomInfo: roomInfo, invitation: invitation)
self.playCallingBellAndVibration()
return
}
}
self.getInvitationListCursor = cursor
if !cursor.isEmpty {
self.getInvitationList(roomInfo: roomInfo)
}
} onError: { error, message in
}
}
} }
extension InvitationObserverService: TUIConferenceInvitationObserver { extension InvitationObserverService: TUIConferenceInvitationObserver {
func onReceiveInvitation(roomInfo: TUIRoomInfo, invitation: TUIInvitation, extensionInfo: String) { public func onReceiveInvitation(roomInfo: TUIRoomInfo, invitation: TUIInvitation, extensionInfo: String) {
let store = Container.shared.conferenceStore() let store = Container.shared.conferenceStore()
store.dispatch(action: ConferenceInvitationActions.onReceiveInvitation(payload: (roomInfo, invitation))) store.dispatch(action: ConferenceInvitationActions.onReceiveInvitation(payload: (roomInfo, invitation)))
} }
func onInvitationHandledByOtherDevice(roomInfo: TUIRoomInfo, accepted: Bool) { public func onInvitationHandledByOtherDevice(roomInfo: TUIRoomInfo, accepted: Bool) {
} }
func onInvitationCancelled(roomInfo: TUIRoomInfo, invitation: TUIInvitation) { public func onInvitationCancelled(roomInfo: TUIRoomInfo, invitation: TUIInvitation) {
} }
func onInvitationAccepted(roomInfo: TUIRoomInfo, invitation: TUIInvitation) { public func onInvitationAccepted(roomInfo: TUIRoomInfo, invitation: TUIInvitation) {
} }
func onInvitationRejected(roomInfo: TUIRoomInfo, invitation: TUIInvitation, reason: TUIInvitationRejectedReason) { public func onInvitationRejected(roomInfo: TUIRoomInfo, invitation: TUIInvitation, reason: TUIInvitationRejectedReason) {
} }
func onInvitationTimeout(roomInfo: TUIRoomInfo, invitation: TUIInvitation) { public func onInvitationTimeout(roomInfo: TUIRoomInfo, invitation: TUIInvitation) {
} }
func onInvitationRevokedByAdmin(roomInfo: TUIRoomInfo, invitation: TUIInvitation, admin: TUIUserInfo) { public func onInvitationRevokedByAdmin(roomInfo: TUIRoomInfo, invitation: TUIInvitation, admin: TUIUserInfo) {
} }
func onInvitationAdded(roomId: String, invitation: TUIInvitation) { public func onInvitationAdded(roomId: String, invitation: TUIInvitation) {
} }
func onInvitationRemoved(roomId: String, invitation: TUIInvitation) { public func onInvitationRemoved(roomId: String, invitation: TUIInvitation) {
} }
func onInvitationStatusChanged(roomId: String, invitation: TUIInvitation) { public func onInvitationStatusChanged(roomId: String, invitation: TUIInvitation) {
} }
} }
......
...@@ -18,5 +18,6 @@ class ServiceCenter: NSObject { ...@@ -18,5 +18,6 @@ class ServiceCenter: NSObject {
let conferenceListService = ConferenceListService() let conferenceListService = ConferenceListService()
let roomService = RoomService() let roomService = RoomService()
let conferenceInvitationService = ConferenceInvitationService() let conferenceInvitationService = ConferenceInvitationService()
let audioService = AudioService()
} }
...@@ -16,11 +16,13 @@ struct RoomInfo { ...@@ -16,11 +16,13 @@ struct RoomInfo {
var ownerAvatarUrl = "" var ownerAvatarUrl = ""
var isSeatEnabled = false var isSeatEnabled = false
var password = "" var password = ""
var isMicrophoneDisableForAllUser = true var isMicrophoneDisableForAllUser = false
var isCameraDisableForAllUser = true var isCameraDisableForAllUser = false
var isScreenShareDisableForAllUser = false
var createTime: UInt = 0 var createTime: UInt = 0
var isPasswordEnabled: Bool = false var isPasswordEnabled: Bool = false
var isEnteredRoom = false var isEnteredRoom = false
var memberCount = 0
init() {} init() {}
init(with roomInfo: TUIRoomInfo) { init(with roomInfo: TUIRoomInfo) {
...@@ -35,6 +37,8 @@ struct RoomInfo { ...@@ -35,6 +37,8 @@ struct RoomInfo {
self.isCameraDisableForAllUser = roomInfo.isCameraDisableForAllUser self.isCameraDisableForAllUser = roomInfo.isCameraDisableForAllUser
self.createTime = roomInfo.createTime self.createTime = roomInfo.createTime
self.isPasswordEnabled = roomInfo.password.count > 0 self.isPasswordEnabled = roomInfo.password.count > 0
self.memberCount = roomInfo.memberCount
self.isScreenShareDisableForAllUser = roomInfo.isScreenShareDisableForAllUser
} }
} }
...@@ -48,5 +52,6 @@ extension TUIRoomInfo { ...@@ -48,5 +52,6 @@ extension TUIRoomInfo {
self.isMicrophoneDisableForAllUser = roomInfo.isMicrophoneDisableForAllUser self.isMicrophoneDisableForAllUser = roomInfo.isMicrophoneDisableForAllUser
self.isCameraDisableForAllUser = roomInfo.isCameraDisableForAllUser self.isCameraDisableForAllUser = roomInfo.isCameraDisableForAllUser
self.seatMode = .applyToTake self.seatMode = .applyToTake
self.isScreenShareDisableForAllUser = roomInfo.isScreenShareDisableForAllUser
} }
} }
...@@ -22,8 +22,8 @@ enum ConferenceInvitationActions { ...@@ -22,8 +22,8 @@ enum ConferenceInvitationActions {
// MARK: callback // MARK: callback
static let updateInvitationList = ActionTemplate(id: key.appending(".setInvitationList"), payloadType: [TUIInvitation].self) static let updateInvitationList = ActionTemplate(id: key.appending(".setInvitationList"), payloadType: [TUIInvitation].self)
static let addInvitation = ActionTemplate(id: key.appending(".addInvitation"), payloadType: TUIInvitation.self) static let addInvitation = ActionTemplate(id: key.appending(".addInvitation"), payloadType: TUIInvitation.self)
static let removeInvitation = ActionTemplate(id: key.appending(".addInvitation"), payloadType: String.self) static let removeInvitation = ActionTemplate(id: key.appending(".removeInvitation"), payloadType: String.self)
static let changeInvitationStatus = ActionTemplate(id: key.appending(".addInvitation"), payloadType: TUIInvitation.self) static let changeInvitationStatus = ActionTemplate(id: key.appending(".changeInvitationStatus"), payloadType: TUIInvitation.self)
static let onInviteSuccess = ActionTemplate(id: key.appending("onInviteSuccess")) static let onInviteSuccess = ActionTemplate(id: key.appending("onInviteSuccess"))
static let onAcceptSuccess = ActionTemplate(id: key.appending("onAcceptSuccess"), payloadType: String.self) static let onAcceptSuccess = ActionTemplate(id: key.appending("onAcceptSuccess"), payloadType: String.self)
static let onRejectSuccess = ActionTemplate(id: key.appending("onRejectSuccess")) static let onRejectSuccess = ActionTemplate(id: key.appending("onRejectSuccess"))
......
...@@ -36,6 +36,7 @@ class ConferenceInvitationEffects: Effects { ...@@ -36,6 +36,7 @@ class ConferenceInvitationEffects: Effects {
} }
.catch { error -> Just<Action> in .catch { error -> Just<Action> in
environment.store?.dispatch(action: InvitationViewActions.dismissInvitationView()) environment.store?.dispatch(action: InvitationViewActions.dismissInvitationView())
environment.store?.dispatch(action: InvitationObserverActions.stopCallingBellAndVibration())
return Just(ErrorActions.throwError(payload: error)) return Just(ErrorActions.throwError(payload: error))
} }
} }
...@@ -112,6 +113,7 @@ class ConferenceInvitationEffects: Effects { ...@@ -112,6 +113,7 @@ class ConferenceInvitationEffects: Effects {
RoomRouter.shared.push(viewController: vc) RoomRouter.shared.push(viewController: vc)
} }
environment.store?.dispatch(action: InvitationViewActions.dismissInvitationView()) environment.store?.dispatch(action: InvitationViewActions.dismissInvitationView())
environment.store?.dispatch(action: InvitationObserverActions.stopCallingBellAndVibration())
} }
} }
...@@ -128,7 +130,8 @@ class ConferenceInvitationEffects: Effects { ...@@ -128,7 +130,8 @@ class ConferenceInvitationEffects: Effects {
} else if isNotBeingInviting == false { } else if isNotBeingInviting == false {
environment.store?.dispatch(action: ConferenceInvitationActions.reject(payload: (roomInfo.roomId, .rejectToEnter))) environment.store?.dispatch(action: ConferenceInvitationActions.reject(payload: (roomInfo.roomId, .rejectToEnter)))
} else { } else {
InvitationObserverService.shared.showInvitationWindow(roomInfo: roomInfo, invitation: invitation) InvitationObserverService.shared.showInvitationWindow(roomInfo: RoomInfo(with: roomInfo), invitation: invitation)
environment.store?.dispatch(action: InvitationObserverActions.playCallingBellAndVibration())
} }
} }
} }
......
...@@ -16,6 +16,7 @@ protocol ConferenceStore: ActionDispatcher { ...@@ -16,6 +16,7 @@ protocol ConferenceStore: ActionDispatcher {
var errorSubject: PassthroughSubject<RoomError, Never> { get } var errorSubject: PassthroughSubject<RoomError, Never> { get }
var toastSubject: PassthroughSubject<ToastInfo, Never> { get } var toastSubject: PassthroughSubject<ToastInfo, Never> { get }
var scheduleActionSubject: PassthroughSubject<IdentifiableAction, Never> { get } var scheduleActionSubject: PassthroughSubject<IdentifiableAction, Never> { get }
var roomActionSubject: PassthroughSubject<IdentifiableAction, Never> { get }
func select<Value:Equatable>(_ selector: Selector<OperationState, Value>) -> AnyPublisher<Value, Never> func select<Value:Equatable>(_ selector: Selector<OperationState, Value>) -> AnyPublisher<Value, Never>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment