RoomVideoFloatViewModel.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. //
  2. // RoomVideoFloatViewModel.swift
  3. // TUIRoomKit
  4. //
  5. // Created by janejntang on 2023/7/11.
  6. //
  7. import Foundation
  8. import RTCRoomEngine
  9. import Factory
  10. import Combine
  11. protocol RoomVideoFloatViewResponder: NSObject {
  12. func updateUserStatus(user: UserEntity)
  13. func makeToast(text: String)
  14. func showAvatarImageView(isShow: Bool)
  15. func getRenderView() -> UIView
  16. func updateUserVolume(volume: Int)
  17. func updateUserAudio(hasAudio: Bool)
  18. }
  19. class RoomVideoFloatViewModel: NSObject {
  20. var displayUserId: String = ""
  21. var displayStreamType: TUIVideoStreamType = .cameraStream
  22. weak var viewResponder: RoomVideoFloatViewResponder?
  23. private var lastSwitchTime = 0
  24. private var switchIntervalTime = 5
  25. private let voiceVolumeMinLimit = 10
  26. var engineManager: EngineManager {
  27. EngineManager.shared
  28. }
  29. var roomInfo: TUIRoomInfo {
  30. engineManager.store.roomInfo
  31. }
  32. var currentUser: UserEntity {
  33. engineManager.store.currentUser
  34. }
  35. override init() {
  36. super.init()
  37. subscribeEngine()
  38. subLogoutNotification()
  39. }
  40. private func subscribeEngine() {
  41. EngineEventCenter.shared.subscribeEngine(event: .onUserVideoStateChanged, observer: self)
  42. EngineEventCenter.shared.subscribeEngine(event: .onRoomDismissed, observer: self)
  43. EngineEventCenter.shared.subscribeEngine(event: .onKickedOutOfRoom, observer: self)
  44. EngineEventCenter.shared.subscribeEngine(event: .onUserAudioStateChanged, observer: self)
  45. EngineEventCenter.shared.subscribeEngine(event: .onUserVoiceVolumeChanged, observer: self)
  46. EngineEventCenter.shared.subscribeEngine(event: .onKickedOffLine, observer: self)
  47. EngineEventCenter.shared.subscribeEngine(event: .onRemoteUserLeaveRoom, observer: self)
  48. EngineEventCenter.shared.subscribeEngine(event: .onUserInfoChanged, observer: self)
  49. }
  50. private func unsubscribeEngine() {
  51. EngineEventCenter.shared.unsubscribeEngine(event: .onUserVideoStateChanged, observer: self)
  52. EngineEventCenter.shared.unsubscribeEngine(event: .onRoomDismissed, observer: self)
  53. EngineEventCenter.shared.unsubscribeEngine(event: .onKickedOutOfRoom, observer: self)
  54. EngineEventCenter.shared.unsubscribeEngine(event: .onUserAudioStateChanged, observer: self)
  55. EngineEventCenter.shared.unsubscribeEngine(event: .onUserVoiceVolumeChanged, observer: self)
  56. EngineEventCenter.shared.unsubscribeEngine(event: .onKickedOffLine, observer: self)
  57. EngineEventCenter.shared.unsubscribeEngine(event: .onRemoteUserLeaveRoom, observer: self)
  58. EngineEventCenter.shared.unsubscribeEngine(event: .onUserInfoChanged, observer: self)
  59. }
  60. private func subLogoutNotification() {
  61. NotificationCenter.default.addObserver(self,
  62. selector: #selector(dismissFloatViewForLogout),
  63. name: NSNotification.Name.TUILogoutSuccess, object: nil)
  64. }
  65. private func unsubLogoutNotification() {
  66. NotificationCenter.default.removeObserver(self, name: NSNotification.Name.TUILogoutSuccess, object: nil)
  67. }
  68. func showRoomMainView() {
  69. if engineManager.store.isEnteredRoom {
  70. EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ShowRoomMainView, param: [:])
  71. }
  72. }
  73. func showFloatWindowViewVideo() {
  74. if let userItem = getScreenUserItem() {
  75. switchScreenStreamUser(userItem: userItem)
  76. } else if getRoomOwnerUser() != nil {
  77. switchToRoomOwnerUser()
  78. } else {
  79. switchToSelfUser()
  80. }
  81. }
  82. func getUserEntity(userId: String) -> UserEntity? {
  83. return engineManager.store.attendeeList.first(where: { $0.userId == userId })
  84. }
  85. @objc private func dismissFloatViewForLogout() {
  86. RoomVideoFloatView.dismiss()
  87. RoomRouter.makeToastInWindow(toast: .logoutText, duration: 0.5)
  88. }
  89. func reportFloatWindowShow() {
  90. RoomKitReport.reportData(.metricsFloatWindowShow)
  91. }
  92. deinit {
  93. unsubscribeEngine()
  94. unsubLogoutNotification()
  95. debugPrint("deinit \(self)")
  96. }
  97. @Injected(\.conferenceStore) var operation: ConferenceStore
  98. }
  99. extension RoomVideoFloatViewModel {
  100. private func getScreenUserItem() -> UserEntity? {
  101. return engineManager.store.attendeeList.first(where: { $0.hasScreenStream == true })
  102. }
  103. private func showScreenStream(userItem: UserEntity) {
  104. startPlayVideo(userId: userItem.userId, streamType: .screenStream)
  105. changePlayingState(userId: userItem.userId, streamType: .screenStream)
  106. viewResponder?.updateUserStatus(user: userItem)
  107. viewResponder?.showAvatarImageView(isShow: false)
  108. }
  109. private func showCameraStream(userItem: UserEntity) {
  110. viewResponder?.updateUserStatus(user: userItem)
  111. if userItem.hasVideoStream {
  112. startPlayVideo(userId: userItem.userId, streamType: .cameraStream)
  113. } else {
  114. viewResponder?.showAvatarImageView(isShow: true)
  115. }
  116. changePlayingState(userId: userItem.userId, streamType: .cameraStream)
  117. }
  118. private func isArrivalSwitchUserTime() -> Bool {
  119. let currentTime = Int(Date().timeIntervalSince1970)
  120. return labs(currentTime - lastSwitchTime) > switchIntervalTime
  121. }
  122. private func getMaxVoiceVolumeMap(volumeMap: [String : NSNumber]) -> [String : Int]? {
  123. guard let maxElement = volumeMap.max(by: { $0.value.compare($1.value) == .orderedAscending }) else { return nil }
  124. return [maxElement.key : maxElement.value.intValue]
  125. }
  126. private func isSwitchOriginalUser(originalUserId: String, volumeMap: [String : NSNumber]) -> Bool {
  127. let originalUserVolume = volumeMap[originalUserId]?.intValue ?? 0
  128. guard originalUserVolume < voiceVolumeMinLimit else { return false }
  129. guard let maxVolume = getMaxVoiceVolumeMap(volumeMap: volumeMap)?.values.first else { return false }
  130. return maxVolume >= voiceVolumeMinLimit
  131. }
  132. private func switchToRoomOwnerUser() {
  133. guard let ownerUser = getRoomOwnerUser() else { return }
  134. if displayUserId.isEmpty {
  135. showCameraStream(userItem: ownerUser)
  136. } else if displayUserId != ownerUser.userId || displayStreamType != .cameraStream {
  137. stopPlayVideo(userId: displayUserId, streamType: displayStreamType)
  138. showCameraStream(userItem: ownerUser)
  139. }
  140. }
  141. private func switchToSelfUser() {
  142. if displayUserId.isEmpty {
  143. showCameraStream(userItem: currentUser)
  144. } else if displayUserId != currentUser.userId || displayStreamType != .cameraStream {
  145. stopPlayVideo(userId: displayUserId, streamType: displayStreamType)
  146. showCameraStream(userItem: currentUser)
  147. }
  148. }
  149. private func getRoomOwnerUser() -> UserEntity? {
  150. return engineManager.store.attendeeList.first(where: { $0.userRole == .roomOwner })
  151. }
  152. private func switchCameraStreamUser(volumeMap: [String : NSNumber]) {
  153. guard !volumeMap.isEmpty else { return }
  154. guard isArrivalSwitchUserTime() else { return }
  155. var latestDisplayUserId: String
  156. if !isSwitchOriginalUser(originalUserId: displayUserId, volumeMap: volumeMap) {
  157. latestDisplayUserId = displayUserId
  158. } else if let maxVolumeMap = getMaxVoiceVolumeMap(volumeMap: volumeMap)?.first, maxVolumeMap.value >= voiceVolumeMinLimit {
  159. latestDisplayUserId = maxVolumeMap.key
  160. } else if let user = getRoomOwnerUser() {
  161. latestDisplayUserId = user.userId
  162. } else {
  163. latestDisplayUserId = currentUser.userId
  164. }
  165. guard let latestDisplayUserItem = getUserEntity(userId: latestDisplayUserId) else { return }
  166. if displayUserId.isEmpty {
  167. showCameraStream(userItem: latestDisplayUserItem)
  168. lastSwitchTime = Int(Date().timeIntervalSince1970)
  169. } else if latestDisplayUserId != displayUserId || displayStreamType != .cameraStream {
  170. stopPlayVideo(userId: displayUserId, streamType: displayStreamType)
  171. showCameraStream(userItem: latestDisplayUserItem)
  172. lastSwitchTime = Int(Date().timeIntervalSince1970)
  173. }
  174. }
  175. private func switchScreenStreamUser(userItem: UserEntity) {
  176. if !displayUserId.isEmpty {
  177. stopPlayVideo(userId: displayUserId, streamType: displayStreamType)
  178. }
  179. if userItem.userId == currentUser.userId {
  180. showCameraStream(userItem: userItem)
  181. } else {
  182. showScreenStream(userItem: userItem)
  183. }
  184. lastSwitchTime = Int(Date().timeIntervalSince1970)
  185. }
  186. private func startPlayVideo(userId: String, streamType: TUIVideoStreamType) {
  187. if userId == currentUser.userId {
  188. engineManager.setLocalVideoView(viewResponder?.getRenderView())
  189. } else {
  190. engineManager.setRemoteVideoView(userId: userId, streamType: streamType, view: viewResponder?.getRenderView())
  191. engineManager.startPlayRemoteVideo(userId: userId, streamType: streamType)
  192. }
  193. viewResponder?.showAvatarImageView(isShow: false)
  194. }
  195. private func stopPlayVideo(userId: String, streamType: TUIVideoStreamType) {
  196. if userId == currentUser.userId {
  197. engineManager.setLocalVideoView(nil)
  198. return
  199. }
  200. engineManager.setRemoteVideoView(userId: userId, streamType: streamType, view: nil)
  201. guard let userItem = getUserEntity(userId: userId) else { return }
  202. if streamType == .screenStream, userItem.hasScreenStream {
  203. engineManager.stopPlayRemoteVideo(userId: userId, streamType: .screenStream)
  204. } else if streamType == .cameraStream, userItem.hasVideoStream {
  205. engineManager.stopPlayRemoteVideo(userId: userId, streamType: .cameraStream)
  206. }
  207. }
  208. private func changePlayingState(userId: String, streamType: TUIVideoStreamType) {
  209. self.displayUserId = userId
  210. self.displayStreamType = streamType
  211. }
  212. }
  213. extension RoomVideoFloatViewModel: RoomEngineEventResponder {
  214. func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
  215. switch name {
  216. case .onRoomDismissed:
  217. handleRoomDismissed()
  218. case .onKickedOutOfRoom:
  219. handleKickedOutOfRoom()
  220. case .onUserVideoStateChanged:
  221. guard let userId = param?["userId"] as? String else { return }
  222. guard let streamType = param?["streamType"] as? TUIVideoStreamType else { return }
  223. guard let hasVideo = param?["hasVideo"] as? Bool else { return }
  224. handleUserVideoStateChanged(userId: userId, streamType: streamType, hasVideo: hasVideo)
  225. case .onUserAudioStateChanged:
  226. guard let userId = param?["userId"] as? String else { return }
  227. guard let hasAudio = param?["hasAudio"] as? Bool else { return }
  228. handleUserAudioStateChanged(userId: userId, hasAudio: hasAudio)
  229. case .onUserVoiceVolumeChanged:
  230. guard let volumeMap = param as? [String : NSNumber] else { return }
  231. handleUserVoiceVolumeChanged(volumeMap: volumeMap)
  232. case .onKickedOffLine:
  233. handleKickedOffLine()
  234. case .onRemoteUserLeaveRoom :
  235. guard let userInfo = param?["userInfo"] as? TUIUserInfo else { return }
  236. handleRemoteUserLeaveRoom(useInfo: userInfo)
  237. case .onUserInfoChanged:
  238. guard let userInfo = param?["userInfo"] as? TUIUserInfo else { return }
  239. guard let modifyFlag = param?["modifyFlag"] as? TUIUserInfoModifyFlag else { return }
  240. if modifyFlag.contains(.nameCard) {
  241. guard userInfo.userId == displayUserId else { return }
  242. guard let userItem = getUserEntity(userId: displayUserId) else { return }
  243. viewResponder?.updateUserStatus(user: userItem)
  244. } else if modifyFlag.contains(.userRole) {
  245. handleUserRoleChanged(userId: userInfo.userId, userRole: userInfo.userRole)
  246. }
  247. default: break
  248. }
  249. }
  250. private func handleRoomDismissed() {
  251. engineManager.destroyEngineManager()
  252. RoomVideoFloatView.dismiss()
  253. RoomRouter.makeToastInWindow(toast: .roomDismissedText, duration: 0.5)
  254. }
  255. private func handleKickedOutOfRoom() {
  256. engineManager.destroyEngineManager()
  257. RoomVideoFloatView.dismiss()
  258. operation.dispatch(action: RoomResponseActions.onExitSuccess())
  259. RoomRouter.makeToastInWindow(toast: .kickedOutOfRoomText, duration: 0.5)
  260. }
  261. private func handleUserVideoStateChanged(userId: String, streamType: TUIVideoStreamType, hasVideo: Bool) {
  262. if streamType == .screenStream {
  263. if hasVideo {
  264. guard let userItem = getUserEntity(userId: userId) else { return }
  265. switchScreenStreamUser(userItem: userItem)
  266. } else if getRoomOwnerUser() != nil {
  267. switchToRoomOwnerUser()
  268. } else {
  269. switchToSelfUser()
  270. }
  271. return
  272. }
  273. guard getScreenUserItem() == nil || getScreenUserItem()?.userId == currentUser.userId else { return }
  274. guard userId == displayUserId else { return }
  275. if hasVideo {
  276. startPlayVideo(userId: userId, streamType: streamType)
  277. } else {
  278. viewResponder?.showAvatarImageView(isShow: true)
  279. }
  280. }
  281. private func handleUserAudioStateChanged(userId: String, hasAudio: Bool) {
  282. guard userId == displayUserId else { return }
  283. viewResponder?.updateUserAudio(hasAudio: hasAudio)
  284. }
  285. private func handleUserVoiceVolumeChanged(volumeMap: [String : NSNumber]) {
  286. if getScreenUserItem() == nil {
  287. switchCameraStreamUser(volumeMap: volumeMap)
  288. }
  289. let userVolume = volumeMap[displayUserId]?.intValue ?? 0
  290. viewResponder?.updateUserVolume(volume: userVolume)
  291. }
  292. private func handleKickedOffLine() {
  293. RoomVideoFloatView.dismiss()
  294. operation.dispatch(action: RoomResponseActions.onExitSuccess())
  295. RoomRouter.makeToastInWindow(toast: .kickedOffLineText, duration: 0.5)
  296. }
  297. private func handleRemoteUserLeaveRoom(useInfo: TUIUserInfo) {
  298. guard displayUserId == useInfo.userId else { return }
  299. if getRoomOwnerUser() != nil {
  300. switchToRoomOwnerUser()
  301. } else {
  302. switchToSelfUser()
  303. }
  304. }
  305. private func handleUserRoleChanged(userId: String, userRole: TUIRole) {
  306. if getScreenUserItem() == nil, userRole == .roomOwner {
  307. switchToRoomOwnerUser()
  308. }
  309. guard let userItem = getUserEntity(userId: displayUserId) else { return }
  310. viewResponder?.updateUserStatus(user: userItem)
  311. }
  312. }
  313. private extension String {
  314. static let logoutText = localized("You are logged out")
  315. static let roomDismissedText = localized("The conference was closed.")
  316. static let kickedOutOfRoomText = localized("You were removed by the host.")
  317. static let kickedOffLineText = localized("You are already logged in elsewhere")
  318. }