VideoSeatState.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. //
  2. // VideoSeatState.swift
  3. // TUIRoomKit
  4. //
  5. // Created by CY zhao on 2024/10/23.
  6. //
  7. import Foundation
  8. import Combine
  9. import RTCRoomEngine
  10. struct VideoSeatState {
  11. var videoSeatItems: [UserInfo] = []
  12. var offSeatItems : [UserInfo] = []
  13. var shareItem: UserInfo?
  14. var speakerItem: UserInfo?
  15. var isSelfScreenSharing = false
  16. }
  17. protocol VideoStore {
  18. var videoState: VideoSeatState { get }
  19. func subscribe<Value>(_ selector: Selector<VideoSeatState, Value>) -> AnyPublisher<Value, Never>
  20. var speakerChangedSubject: PassthroughSubject<UserInfo?, Never> { get }
  21. }
  22. class VideoStoreProvider: NSObject {
  23. private var lastSwitchTime = 0
  24. private var switchIntervalTime = 5
  25. private let voiceVolumeMinLimit = 10
  26. let speakerChangedSubject = PassthroughSubject<UserInfo?, Never>()
  27. // TODO: remove shared RoomStore
  28. var roomStore: RoomStore {
  29. EngineManager.shared.store
  30. }
  31. private lazy var store: Store<VideoSeatState, Void> = {
  32. let store = Store.init(initialState: VideoSeatState(), reducers: [VideoStateUpdater])
  33. return store
  34. }()
  35. private(set) var roomEngine = TUIRoomEngine.sharedInstance()
  36. private var cancellables = Set<AnyCancellable>()
  37. override init() {
  38. super.init()
  39. initVideoItems()
  40. initSubscriptions()
  41. roomEngine.addObserver(self)
  42. }
  43. private func initSubscriptions() {
  44. EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_RenewVideoSeatView, responder: self)
  45. }
  46. private func initVideoItems() {
  47. let videoItems = roomStore.roomInfo.isSeatEnabled ? roomStore.seatList : roomStore.attendeeList
  48. guard videoItems.count > 0 else { return }
  49. if !roomStore.roomInfo.isSeatEnabled {
  50. RoomKitLog.info("\(#file)","\(#line)","init userList: \(videoItems.map{ $0.userId })")
  51. self.initVideoItems(items: videoItems)
  52. } else {
  53. RoomKitLog.info("\(#file)","\(#line)","init seatList: \(videoItems.map{ $0.userId })")
  54. self.initVideoItems(items: videoItems)
  55. self.initOffSeatUsers(users: roomStore.attendeeList.filter({ !$0.isOnSeat }))
  56. }
  57. if let shareInfo = roomStore.attendeeList.first(where: { $0.hasScreenStream }) {
  58. if shareInfo.userId != roomStore.currentUser.userId {
  59. shareInfo.videoStreamType = .screenStream
  60. self.initShareItem(item: shareInfo)
  61. } else {
  62. updateIsSelfScreenSharing(true)
  63. }
  64. }
  65. }
  66. deinit {
  67. roomEngine.removeObserver(self)
  68. EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_RenewVideoSeatView, responder: self)
  69. }
  70. }
  71. extension VideoStoreProvider: RoomKitUIEventResponder {
  72. func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
  73. switch key {
  74. case .TUIRoomKitService_RenewVideoSeatView:
  75. initVideoItems()
  76. default: break
  77. }
  78. }
  79. }
  80. extension VideoStoreProvider: VideoStore {
  81. var videoState: VideoSeatState {
  82. return store.state
  83. }
  84. func subscribe<Value>(_ selector: Selector<VideoSeatState, Value>) -> AnyPublisher<Value, Never> {
  85. return store.select(selector)
  86. }
  87. }
  88. extension VideoStoreProvider: TUIRoomObserver {
  89. func onUserAudioStateChanged(userId: String, hasAudio: Bool, reason: TUIChangeReason) {
  90. guard var item = videoState.videoSeatItems.first(where: { $0.userId == userId }) else { return }
  91. item.hasAudioStream = hasAudio
  92. updateVideoItem(item: item)
  93. if var shareItem = videoState.shareItem, shareItem.userId == item.userId {
  94. shareItem.hasAudioStream = item.hasAudioStream
  95. updateShareItem(item: shareItem)
  96. }
  97. }
  98. func onUserVoiceVolumeChanged(volumeMap: [String : NSNumber]) {
  99. guard volumeMap.count > 0 else { return }
  100. for (userId, volume) in volumeMap {
  101. guard var item = videoState.videoSeatItems.first(where: { $0.userId == userId }) else { continue }
  102. item.userVoiceVolume = volume.intValue
  103. updateVideoItem(item: item)
  104. if var shareItem = videoState.shareItem, shareItem.userId == userId {
  105. shareItem.userVoiceVolume = volume.intValue
  106. updateShareItem(item: shareItem)
  107. }
  108. }
  109. switchSpeakerItem(volumeMap: volumeMap)
  110. }
  111. func onUserVideoStateChanged(userId: String, streamType: TUIVideoStreamType, hasVideo: Bool, reason: TUIChangeReason) {
  112. RoomKitLog.info("\(#file)","\(#line)","onUserVideoStateChanged userId: \(userId), streamType: \(streamType.rawValue), hasVideo: \(hasVideo), reason: \(reason.rawValue)")
  113. guard var item = videoState.videoSeatItems.first(where: { $0.userId == userId }) else { return }
  114. if streamType == .screenStream && userId == roomStore.currentUser.userId {
  115. updateIsSelfScreenSharing(hasVideo)
  116. return
  117. }
  118. if streamType == .cameraStream || streamType == .cameraStreamLow {
  119. item.hasVideoStream = hasVideo
  120. updateVideoItem(item: item)
  121. } else {
  122. item.videoStreamType = .screenStream
  123. if hasVideo {
  124. updateShareItem(item: item)
  125. } else if userId == videoState.shareItem?.userId {
  126. updateShareItem(item: nil)
  127. }
  128. }
  129. }
  130. func onUserScreenCaptureStopped(reason: Int) {
  131. updateIsSelfScreenSharing(false)
  132. }
  133. func onRemoteUserEnterRoom(roomId: String, userInfo: TUIUserInfo) {
  134. let item = UserInfo(userInfo: userInfo)
  135. if !roomStore.roomInfo.isSeatEnabled {
  136. addVideoItem(item: item)
  137. } else {
  138. addOffSeatUser(user: item)
  139. }
  140. }
  141. func onRemoteUserLeaveRoom(roomId: String, userInfo: TUIUserInfo) {
  142. removeVideoItem(userId: userInfo.userId)
  143. removeOffSeatUser(userId: userInfo.userId)
  144. if videoState.speakerItem?.userId == userInfo.userId {
  145. updateSpeakerItem(item: nil)
  146. }
  147. if videoState.shareItem?.userId == userInfo.userId {
  148. updateShareItem(item: nil)
  149. }
  150. }
  151. func onUserInfoChanged(userInfo: TUIUserInfo, modifyFlag: TUIUserInfoModifyFlag) {
  152. if modifyFlag.contains(.nameCard) {
  153. onUserNameCardChanged(userInfo: userInfo)
  154. } else if modifyFlag.contains(.userRole) {
  155. onUserRoleChanged(userInfo: userInfo)
  156. }
  157. }
  158. private func onUserRoleChanged(userInfo: TUIUserInfo) {
  159. if var item = videoState.videoSeatItems.first(where: { $0.userId == userInfo.userId }) {
  160. item.userRole = userInfo.userRole
  161. updateVideoItem(item: item)
  162. }
  163. if var offSeatItem = videoState.offSeatItems.first(where: { $0.userId == userInfo.userId }) {
  164. offSeatItem.userRole = userInfo.userRole
  165. store.dispatch(action: VideoActions.updateOffseatItem(payload: offSeatItem))
  166. }
  167. if var shareItem = videoState.shareItem, shareItem.userId == userInfo.userId {
  168. shareItem.userRole = userInfo.userRole
  169. updateShareItem(item: shareItem)
  170. }
  171. }
  172. private func onUserNameCardChanged(userInfo: TUIUserInfo) {
  173. guard var item = videoState.videoSeatItems.first(where: { $0.userId == userInfo.userId }) else { return }
  174. item.userName = userInfo.nameCard
  175. updateVideoItem(item: item)
  176. if var shareItem = videoState.shareItem, shareItem.userId == userInfo.userId {
  177. shareItem.userName = userInfo.nameCard
  178. updateShareItem(item: shareItem)
  179. }
  180. }
  181. func onSeatListChanged(seatList: [TUISeatInfo], seated seatedList: [TUISeatInfo], left leftList: [TUISeatInfo]) {
  182. updateLeftSeatList(leftList: leftList)
  183. updateSeatedList(seatList: seatedList)
  184. }
  185. private func updateLeftSeatList(leftList: [TUISeatInfo]) {
  186. guard leftList.count > 0 else { return }
  187. for seatInfo: TUISeatInfo in leftList {
  188. guard let userId = seatInfo.userId else { continue }
  189. if var userItem = videoState.videoSeatItems.first(where: { $0.userId == userId }) {
  190. userItem.hasAudioStream = false
  191. userItem.hasVideoStream = false
  192. addOffSeatUser(user: userItem)
  193. }
  194. removeVideoItem(userId: userId)
  195. if videoState.speakerItem?.userId == userId {
  196. updateSpeakerItem(item: nil)
  197. }
  198. }
  199. }
  200. private func updateSeatedList(seatList: [TUISeatInfo]) {
  201. guard seatList.count > 0 else { return }
  202. for seatInfo: TUISeatInfo in seatList {
  203. guard let userId = seatInfo.userId else { continue }
  204. guard !videoState.videoSeatItems.contains(where: { $0.userId == userId }) else { continue }
  205. if let userItem = videoState.offSeatItems.first(where: { $0.userId == userId }) {
  206. addVideoItem(item: userItem)
  207. } else {
  208. var item = UserInfo()
  209. item.userId = userId
  210. addVideoItem(item: item)
  211. }
  212. removeOffSeatUser(userId: userId)
  213. }
  214. }
  215. }
  216. // MARK: private
  217. extension VideoStoreProvider {
  218. private func switchSpeakerItem(volumeMap: [String : NSNumber]) {
  219. guard !volumeMap.isEmpty else { return }
  220. guard isArrivalSwitchUserTime() else { return }
  221. var currentSpeakerUserId = ""
  222. if let speakerId = volumeMap.first(where: { $0.value.intValue > voiceVolumeMinLimit })?.key {
  223. currentSpeakerUserId = speakerId
  224. }
  225. if let currentSpeakerItem = videoState.videoSeatItems.first(where: { $0.userId == currentSpeakerUserId }) {
  226. updateSpeakerItem(item: currentSpeakerItem)
  227. }
  228. lastSwitchTime = Int(Date().timeIntervalSince1970)
  229. }
  230. private func isArrivalSwitchUserTime() -> Bool {
  231. let currentTime = Int(Date().timeIntervalSince1970)
  232. return labs(currentTime - lastSwitchTime) > switchIntervalTime
  233. }
  234. private func updateVideoItem(item: UserInfo) {
  235. store.dispatch(action: VideoActions.updateVideoItem(payload: item))
  236. }
  237. private func addVideoItem(item: UserInfo) {
  238. guard !videoState.videoSeatItems.contains(where: { $0.userId == item.userId }) else { return }
  239. store.dispatch(action: VideoActions.addVideoItem(payload: item))
  240. }
  241. private func addOffSeatUser(user: UserInfo) {
  242. guard !videoState.offSeatItems.contains(where: { $0.userId == user.userId }) else { return }
  243. store.dispatch(action: VideoActions.addOffSeatUser(payload: user))
  244. }
  245. private func removeOffSeatUser(userId: String) {
  246. store.dispatch(action: VideoActions.removeOffSeatUser(payload: userId))
  247. }
  248. private func removeVideoItem(userId: String) {
  249. store.dispatch(action: VideoActions.removeVideoItem(payload: userId))
  250. }
  251. private func updateShareItem(item: UserInfo?) {
  252. store.dispatch(action: VideoActions.updateShareItem(payload: item))
  253. }
  254. private func updateSpeakerItem(item: UserInfo?) {
  255. store.dispatch(action: VideoActions.updateSpeakerItem(payload: item))
  256. speakerChangedSubject.send(item)
  257. }
  258. private func initVideoItems(items: [UserEntity]) {
  259. store.dispatch(action: VideoActions.initVideoItems(payload: items))
  260. }
  261. private func initOffSeatUsers(users: [UserEntity]) {
  262. store.dispatch(action: VideoActions.initOffSeatUsers(payload: users))
  263. }
  264. private func initShareItem(item: UserEntity) {
  265. store.dispatch(action: VideoActions.initShareItem(payload: item))
  266. }
  267. private func updateIsSelfScreenSharing(_ isSelfScreenSharing: Bool) {
  268. store.dispatch(action: VideoActions.updateIsSelfScreenSharing(payload: isSelfScreenSharing))
  269. }
  270. }
  271. enum VideoActions {
  272. static let key = "video.action"
  273. static let initVideoItems = ActionTemplate(id: key.appending(".initVideoItems"), payloadType: [UserEntity].self)
  274. static let initOffSeatUsers = ActionTemplate(id: key.appending(".initOffSeatUsers"), payloadType: [UserEntity].self)
  275. static let initShareItem = ActionTemplate(id: key.appending(".initShareItem"), payloadType: UserEntity.self)
  276. static let addVideoItem = ActionTemplate(id: key.appending(".addVideoItem"), payloadType: UserInfo.self)
  277. static let removeVideoItem = ActionTemplate(id: key.appending(".removeVieoItem"), payloadType: String.self)
  278. static let addOffSeatUser = ActionTemplate(id: key.appending(".addOffSeatUser"), payloadType: UserInfo.self)
  279. static let removeOffSeatUser = ActionTemplate(id: key.appending(".removeOffSeatUser"), payloadType: String.self)
  280. static let updateVideoItem = ActionTemplate(id: key.appending(".updateVideoItem"), payloadType: UserInfo.self)
  281. static let updateOffseatItem = ActionTemplate(id: key.appending(".updateOffseatItem"), payloadType: UserInfo.self)
  282. static let updateShareItem = ActionTemplate(id: key.appending(".updateShareItem"), payloadType: UserInfo?.self)
  283. static let updateSpeakerItem = ActionTemplate(id: key.appending(".updateSpeakerItem"), payloadType: UserInfo?.self)
  284. static let updateIsSelfScreenSharing = ActionTemplate(id: key.appending(".updateIsSelfScreenSharing"), payloadType: Bool.self)
  285. }
  286. let VideoStateUpdater = Reducer<VideoSeatState> (
  287. ReduceOn(VideoActions.initVideoItems, reduce: { state, action in
  288. let userItems = action.payload.map { UserInfo(userEntity: $0) }
  289. state.videoSeatItems = userItems
  290. }),
  291. ReduceOn(VideoActions.initOffSeatUsers, reduce: { state, action in
  292. let userItems = action.payload.map { UserInfo(userEntity: $0) }
  293. state.offSeatItems = userItems
  294. }),
  295. ReduceOn(VideoActions.initShareItem, reduce: { state, action in
  296. let userInfo = UserInfo(userEntity: action.payload)
  297. state.shareItem = userInfo
  298. }),
  299. ReduceOn(VideoActions.addVideoItem, reduce: { state, action in
  300. state.videoSeatItems.append(action.payload)
  301. }),
  302. ReduceOn(VideoActions.removeVideoItem, reduce: { state, action in
  303. var items = state.videoSeatItems
  304. items.removeAll(where: { $0.userId == action.payload })
  305. state.videoSeatItems = items
  306. }),
  307. ReduceOn(VideoActions.addOffSeatUser, reduce: { state, action in
  308. state.offSeatItems.append(action.payload)
  309. }),
  310. ReduceOn(VideoActions.removeOffSeatUser, reduce: { state, action in
  311. var items = state.offSeatItems
  312. items.removeAll(where: { $0.userId == action.payload })
  313. state.offSeatItems = items
  314. }),
  315. ReduceOn(VideoActions.updateVideoItem, reduce: { state, action in
  316. let item = action.payload
  317. if let index = state.videoSeatItems.firstIndex(where: { $0.userId == item.userId }) {
  318. state.videoSeatItems[index] = item
  319. }
  320. }),
  321. ReduceOn(VideoActions.updateOffseatItem, reduce: { state, action in
  322. let item = action.payload
  323. if let index = state.offSeatItems.firstIndex(where: { $0.userId == item.userId }) {
  324. state.offSeatItems[index] = item
  325. }
  326. }),
  327. ReduceOn(VideoActions.updateShareItem, reduce: { state, action in
  328. state.shareItem = action.payload
  329. }),
  330. ReduceOn(VideoActions.updateSpeakerItem, reduce: { state, action in
  331. state.speakerItem = action.payload
  332. }),
  333. ReduceOn(VideoActions.updateIsSelfScreenSharing, reduce: { state, action in
  334. state.isSelfScreenSharing = action.payload
  335. })
  336. )