Files
featherVoice/TUIKit/TUIRoomKit/Source/View/ViewModel/TUIVideoSeatViewModel.swift

587 lines
24 KiB
Swift
Raw Normal View History

2025-08-08 10:49:36 +08:00
//
// TUIVideoSeatPresenter.swift
// TUIVideoSeat
//
// Created by WesleyLei on 2022/9/28.
// Copyright © 2022 Tencent. All rights reserved.
//
import Foundation
import RTCRoomEngine
#if canImport(TXLiteAVSDK_TRTC)
import TXLiteAVSDK_TRTC
#elseif canImport(TXLiteAVSDK_Professional)
import TXLiteAVSDK_Professional
#endif
protocol TUIVideoSeatViewModelResponder: AnyObject {
func reloadData()
func insertItems(at indexPaths: [IndexPath])
func deleteItems(at indexPaths: [IndexPath])
func getVideoVisibleCell(_ item: VideoSeatItem) -> VideoSeatCell?
func getMoveMiniscreen() -> TUIVideoSeatDragCell
func updateMiniscreen(_ item: VideoSeatItem?)
func updateMiniscreenVolume(_ item: VideoSeatItem)
func updateVideoSeatCellUI(_ item: VideoSeatItem)
func updateSeatVolume(_ item: VideoSeatItem)
func showScreenCaptureMaskView(isShow: Bool)
func destroyVideoSeatResponder()
}
enum TUIVideoSeatViewType {
case unknown
case singleType
case pureAudioType
case largeSmallWindowType
case speechType
case equallyDividedType
}
class TUIVideoSeatViewModel: NSObject {
private var videoSeatItems: [VideoSeatItem] = []
private var shareItem: VideoSeatItem?
private var speakerItem: VideoSeatItem?
private var isSwitchPosition: Bool = false
private var speakerUpdateTimer: Int = 0
private let speakerUpdateTimeInterval = 5
private var itemStreamType: TUIVideoStreamType {
if listSeatItem.filter({ $0.hasVideoStream }).count > 5 {
return .cameraStreamLow
} else {
return .cameraStream
}
}
var listSeatItem: [VideoSeatItem] = []
private var isHasVideoStream: Bool {
return videoSeatItems.firstIndex(where: { $0.isHasVideoStream }) != nil
}
private var isHasScreenStream: Bool {
return shareItem != nil
}
weak var viewResponder: TUIVideoSeatViewModelResponder?
var videoSeatViewType: TUIVideoSeatViewType = .unknown
var engineManager: EngineManager {
EngineManager.shared
}
var store: RoomStore {
engineManager.store
}
var roomInfo: TUIRoomInfo {
store.roomInfo
}
var currentUserId: String {
store.currentUser.userId
}
override init() {
super.init()
initVideoSeatItems()
subscribeUIEvent()
}
private func initVideoSeatItems() {
videoSeatItems = []
let videoItems = store.roomInfo.isSeatEnabled ? store.seatList : store.attendeeList
guard videoItems.count > 0 else { return }
videoItems.forEach { userInfo in
let userItem = VideoSeatItem()
userItem.update(userInfo: userInfo)
videoSeatItems.append(userItem)
}
if let shareInfo = videoItems.first(where: { $0.hasScreenStream }) {
updateShareItem(userInfo: shareInfo)
}
sortSeatItems()
reloadSeatItems()
}
private func subscribeUIEvent() {
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_RenewVideoSeatView, responder: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserAudioStateChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserVideoStateChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserVoiceVolumeChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserScreenCaptureStopped, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onRemoteUserEnterRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onRemoteUserLeaveRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserRoleChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onSeatListChanged, observer: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, responder: self)
}
private func unsubscribeUIEvent() {
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_RenewVideoSeatView, responder: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserAudioStateChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserVideoStateChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserVoiceVolumeChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserScreenCaptureStopped, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onRemoteUserEnterRoom, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onRemoteUserLeaveRoom, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserRoleChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onSeatListChanged, observer: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, responder: self)
}
func updateShareItem(userInfo: UserEntity) {
guard userInfo.hasScreenStream, shareItem == nil else { return }
let item = VideoSeatItem()
item.update(userInfo: userInfo)
item.videoStreamType = .screenStream
shareItem = item
}
deinit {
unsubscribeUIEvent()
debugPrint("deinit:\(self)")
}
}
extension TUIVideoSeatViewModel {
private func startPlayVideo(item: VideoSeatItem, renderView: UIView?) {
guard let renderView = renderView else { return }
if item.userId == currentUserId {
engineManager.setLocalVideoView(streamType: item.videoStreamType, view: renderView)
} else {
item.videoStreamType = item.videoStreamType == .screenStream ? .screenStream : itemStreamType
engineManager.setRemoteVideoView(userId: item.userId, streamType: item.videoStreamType, view: renderView)
engineManager.startPlayRemoteVideo(userId: item.userId, streamType: item.videoStreamType)
}
guard let seatCell = viewResponder?.getVideoVisibleCell(item) else { return }
seatCell.updateUI(item: item)
}
private func stopPlayVideo(item: VideoSeatItem) {
if item.userId == currentUserId {
engineManager.setLocalVideoView(streamType: item.videoStreamType, view: nil)
} else {
engineManager.setRemoteVideoView(userId: item.userId, streamType: item.videoStreamType, view: nil)
engineManager.stopPlayRemoteVideo(userId: item.userId, streamType: item.videoStreamType)
}
guard let seatCell = viewResponder?.getVideoVisibleCell(item) else { return }
seatCell.updateUI(item: item)
}
private func addUserInfo(_ userId: String) {
guard !videoSeatItems.contains(where: { $0.userId == userId }) else { return }
guard let userInfo = getUserInfo(userId: userId) else { return }
let seatItem = VideoSeatItem()
seatItem.update(userInfo: userInfo)
videoSeatItems.append(seatItem)
if checkNeededSort() {
refreshListSeatItem()
viewResponder?.reloadData()
resetMiniscreen()
} else {
reloadSeatItems()
}
}
private func getUserInfo(userId: String) -> UserEntity? {
return store.attendeeList.first(where: { $0.userId == userId })
}
private func removeSeatItem(_ userId: String) {
if shareItem?.userId == userId, let seatItem = shareItem {
stopPlayVideo(item: seatItem)
}
if speakerItem?.userId == userId, let seatItem = speakerItem {
stopPlayVideo(item: seatItem)
}
if let seatItem = videoSeatItems.first(where: { $0.userId == userId }) {
stopPlayVideo(item: seatItem)
}
videoSeatItems.removeAll(where: { $0.userId == userId })
var deleteIndex: [IndexPath] = []
if let index = listSeatItem.firstIndex(where: { $0.userId == userId && $0.videoStreamType != .screenStream }) {
deleteIndex.append(IndexPath(item: index, section: 0))
}
refreshListSeatItem()
if videoSeatViewType == .largeSmallWindowType {
viewResponder?.reloadData()
} else {
viewResponder?.deleteItems(at: deleteIndex)
}
resetMiniscreen()
}
private func changeUserRole(userId: String, userRole: TUIRole) {
if let item = getSeatItem(userId) {
item.userRole = userRole
viewResponder?.updateVideoSeatCellUI(item)
}
if let shareItem = shareItem, shareItem.userId == userId {
shareItem.userRole = userRole
viewResponder?.updateVideoSeatCellUI(shareItem)
}
if let speakerItem = speakerItem, speakerItem.userId == userId {
speakerItem.userRole = userRole
viewResponder?.updateVideoSeatCellUI(speakerItem)
}
guard userRole == .roomOwner else { return }
refreshListSeatItem()
viewResponder?.reloadData()
resetMiniscreen()
}
private func getSeatItem(_ userId: String) -> VideoSeatItem? {
return videoSeatItems.first(where: { $0.userId == userId })
}
private func sortSeatItems() {
guard checkNeededSort() else { return }
// I'm second
if let currentItemIndex = videoSeatItems.firstIndex(where: { $0.userId == self.currentUserId }) {
let currentItem = videoSeatItems.remove(at: currentItemIndex)
videoSeatItems.insert(currentItem, at: 0)
}
// Homeowners always come first
if let roomOwnerItemIndex = videoSeatItems.firstIndex(where: { $0.userId == roomInfo.ownerId }) {
let roomOwnerItem = videoSeatItems.remove(at: roomOwnerItemIndex)
videoSeatItems.insert(roomOwnerItem, at: 0)
}
}
private func checkNeededSort() -> Bool {
var isSort = false
if let roomOwnerItemIndex = videoSeatItems.firstIndex(where: { $0.userId == roomInfo.ownerId }) {
isSort = roomOwnerItemIndex != 0
}
if let currentItemIndex = videoSeatItems.firstIndex(where: { $0.userId == self.currentUserId }) {
if currentUserId == roomInfo.ownerId {
isSort = isSort || (currentItemIndex != 0)
} else {
isSort = isSort || (currentItemIndex != 1)
}
}
return isSort
}
private func findCurrentSpeaker(list: [VideoSeatItem]) -> VideoSeatItem? {
if let shareItem = shareItem {
return list.first(where: { $0.hasAudioStream && $0.userVoiceVolume > 10 && $0.userId != shareItem.userId })
} else if let speech = listSeatItem.first {
return list.first(where:{ $0.hasAudioStream && $0.userVoiceVolume > 10 && $0.userId != speech.userId })
}
return nil
}
private func refreshListSeatItem() {
sortSeatItems()
listSeatItem = Array(videoSeatItems)
if videoSeatItems.count == 1 {
videoSeatViewType = .singleType
if isHasScreenStream {
refreshMultiVideo()
}
} else if videoSeatItems.count == 2, isHasVideoStream, !isHasScreenStream {
videoSeatViewType = .largeSmallWindowType
if isSwitchPosition {
let first = listSeatItem[0]
listSeatItem[0] = listSeatItem[1]
listSeatItem[1] = first
}
} else if videoSeatItems.count >= 2, !isHasVideoStream, !isHasScreenStream {
videoSeatViewType = .pureAudioType
} else {
refreshMultiVideo()
}
}
private func refreshMultiVideo() {
let videoResult = videoSeatItems.filter({ $0.hasVideoStream })
var speechItem: VideoSeatItem?
if let item = shareItem {
speechItem = item
} else if videoResult.count == 1, let item = videoResult.first {
speechItem = item
}
if let item = speechItem, let seatItemIndex = videoSeatItems.firstIndex(where: { $0.userId == item.userId }) {
videoSeatViewType = .speechType
if item.videoStreamType == .screenStream, item.userId != currentUserId {
listSeatItem.insert(item, at: 0)
} else {
listSeatItem.remove(at: seatItemIndex)
listSeatItem.insert(item, at: 0)
if item.userId == speakerItem?.userId {
speakerItem = nil
}
}
if let currentSpeakerItem = findCurrentSpeaker(list: listSeatItem) {
speakerItem = currentSpeakerItem
} else {
if let item = speakerItem, videoSeatItems.firstIndex(where: { $0.userId == item.userId }) == nil {
speakerItem = nil
}
}
} else {
videoSeatViewType = .equallyDividedType
}
}
private func reloadSeatItems() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
let type = self.videoSeatViewType
let lastListSeatItem = Array(self.listSeatItem)
self.refreshListSeatItem()
self.updateCollectionView(type, lastListSeatItem)
self.resetMiniscreen()
}
}
private func resetMiniscreen() {
if self.videoSeatViewType == .speechType {
self.viewResponder?.updateMiniscreen(self.speakerItem)
} else {
self.speakerItem = nil
self.viewResponder?.updateMiniscreen(nil)
}
}
private func updateSeatVolume(item: VideoSeatItem) {
viewResponder?.updateSeatVolume(item)
if let shareItem = shareItem, shareItem.userId == item.userId {
shareItem.hasAudioStream = item.hasAudioStream
shareItem.userVoiceVolume = item.userVoiceVolume
viewResponder?.updateSeatVolume(shareItem)
}
if let speakerItem = speakerItem, speakerItem.userId == item.userId {
speakerItem.hasAudioStream = item.hasAudioStream
speakerItem.userVoiceVolume = item.userVoiceVolume
viewResponder?.updateMiniscreenVolume(speakerItem)
}
}
private func updateCollectionView(_ type: TUIVideoSeatViewType, _ lastList: [VideoSeatItem]) {
if type != videoSeatViewType {
viewResponder?.reloadData()
} else {
let count = lastList.count
let diffItem = listSeatItem.count - count
var indexPaths: [IndexPath] = []
if diffItem > 0 {
for i in count ... (count + diffItem - 1) {
indexPaths.append(IndexPath(item: i, section: 0))
}
viewResponder?.insertItems(at: indexPaths)
}
for i in 0 ... min(max(count - 1, 0), max(listSeatItem.count - 1, 0)) {
guard lastList.count > i && listSeatItem.count > i && lastList[i] != listSeatItem[i] else { continue }
guard let item = listSeatItem[safe: i] else { continue }
viewResponder?.updateVideoSeatCellUI(item)
guard let cell = viewResponder?.getVideoVisibleCell(item) else { continue }
if item.hasVideoStream {
startPlayVideo(item: item, renderView: cell.renderView)
} else {
stopPlayVideo(item: item)
}
}
}
}
private func isConformedSpeakerTimeInterval() -> Bool {
let currentTime: TimeInterval = Date().timeIntervalSince1970
let timeStamp = Int(currentTime)
let totalTime: UInt = UInt(labs(timeStamp - speakerUpdateTimer))
return totalTime > speakerUpdateTimeInterval
}
}
extension TUIVideoSeatViewModel: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
switch key {
case .TUIRoomKitService_RenewVideoSeatView:
initVideoSeatItems()
case .TUIRoomKitService_DismissConferenceViewController:
viewResponder?.destroyVideoSeatResponder()
viewResponder = nil
default: break
}
}
}
extension TUIVideoSeatViewModel: RoomEngineEventResponder {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
switch name {
case .onUserAudioStateChanged:
guard let userId = param?["userId"] as? String else { return }
guard let hasAudio = param?["hasAudio"] as? Bool else { return }
guard let seatItem = getSeatItem(userId) else { return }
seatItem.hasAudioStream = hasAudio
updateSeatVolume(item: seatItem)
case .onUserVoiceVolumeChanged:
guard let volumeMap = param as? [String: NSNumber] else { return }
userVoiceVolumeChanged(volumeMap: volumeMap)
case .onUserVideoStateChanged:
guard let userId = param?["userId"] as? String else { return }
guard let streamType = param?["streamType"] as? TUIVideoStreamType else { return }
guard let hasVideo = param?["hasVideo"] as? Bool else { return }
userVideoStateChanged(userId: userId, streamType: streamType, hasVideo: hasVideo)
case .onUserScreenCaptureStopped:
userScreenCaptureStopped()
case .onRemoteUserEnterRoom:
guard let userInfo = param?["userInfo"] as? TUIUserInfo else { return }
guard !roomInfo.isSeatEnabled else { return }
addUserInfo(userInfo.userId)
case .onRemoteUserLeaveRoom:
guard let userInfo = param?["userInfo"] as? TUIUserInfo else { return }
removeSeatItem(userInfo.userId)
case .onUserRoleChanged:
guard let userId = param?["userId"] as? String else { return }
guard let userRole = param?["userRole"] as? TUIRole else { return }
engineManager.fetchRoomInfo(roomId: roomInfo.roomId) { [weak self] _ in
guard let self = self else { return }
self.changeUserRole(userId: userId, userRole: userRole)
}
case .onSeatListChanged:
guard let left = param?["left"] as? [TUISeatInfo] else { return }
guard let seated = param?["seated"] as? [TUISeatInfo] else { return }
seatListChanged(seated: seated, left: left)
default: break
}
}
}
extension TUIVideoSeatViewModel: TUIRoomObserver {
private func userVoiceVolumeChanged(volumeMap: [String: NSNumber]) {
if volumeMap.count <= 0 {
return
}
for (userId, volume) in volumeMap {
guard let seatItem = getSeatItem(userId) else { continue }
seatItem.userVoiceVolume = volume.intValue
updateSeatVolume(item: seatItem)
}
guard videoSeatViewType == .speechType else { return }
guard let currentSpeakerItem = findCurrentSpeaker(list: listSeatItem) else { return }
if viewResponder?.getMoveMiniscreen().seatItem != nil, speakerItem?.userId == currentSpeakerItem.userId {
viewResponder?.updateMiniscreenVolume(currentSpeakerItem)
} else {
guard isConformedSpeakerTimeInterval() else { return }
viewResponder?.updateMiniscreen(currentSpeakerItem)
speakerUpdateTimer = Int(Date().timeIntervalSince1970)
}
speakerItem = currentSpeakerItem
}
private func userVideoStateChanged(userId: String, streamType: TUIVideoStreamType, hasVideo: Bool) {
if streamType == .screenStream, userId == currentUserId {
viewResponder?.showScreenCaptureMaskView(isShow: hasVideo)
return
}
guard let seatItem = getSeatItem(userId) else { return }
if streamType == .cameraStream || streamType == .cameraStreamLow {
seatItem.hasVideoStream = hasVideo
if hasVideo {
setRemoteRenderParams(userId: userId, streamType: streamType)
startPlayVideo(item: seatItem, renderView: viewResponder?.getVideoVisibleCell(seatItem)?.renderView)
} else {
stopPlayVideo(item: seatItem)
}
reloadSeatItems()
} else {
updateScreenStreamView(seatItem: seatItem, hasVideo: hasVideo)
}
}
private func updateScreenStreamView(seatItem: VideoSeatItem, hasVideo: Bool) {
let screenIndexPath = IndexPath(item: 0, section: 0)
if hasVideo {
guard let shareUserInfo = getUserInfo(userId: seatItem.userId) else { return }
updateShareItem(userInfo: shareUserInfo)
refreshListSeatItem()
viewResponder?.insertItems(at: [screenIndexPath])
} else {
shareItem = nil
refreshListSeatItem()
if videoSeatViewType == .largeSmallWindowType {
viewResponder?.reloadData()
} else {
viewResponder?.deleteItems(at: [screenIndexPath])
}
}
speakerItem = nil
viewResponder?.updateMiniscreen(nil)
}
private func setRemoteRenderParams(userId: String, streamType: TUIVideoStreamType) {
let renderParams = TRTCRenderParams()
renderParams.fillMode = (streamType == .screenStream) ? .fit : .fill
let trtcStreamType: TRTCVideoStreamType = (streamType == .screenStream) ? .sub : .big
engineManager.setRemoteRenderParams(userId: userId, streamType: trtcStreamType, params: renderParams)
}
private func seatListChanged(seated: [TUISeatInfo], left: [TUISeatInfo]) {
for leftSeat in left {
if let userId = leftSeat.userId {
removeSeatItem(userId)
}
}
for seatInfo in seated {
if let userId = seatInfo.userId {
addUserInfo(userId)
}
}
}
private func userScreenCaptureStopped() {
viewResponder?.showScreenCaptureMaskView(isShow: false)
if shareItem?.userId == currentUserId {
shareItem = nil
}
reloadSeatItems()
}
}
extension TUIVideoSeatViewModel: TUIVideoSeatViewResponder {
func switchPosition() {
guard videoSeatViewType == .largeSmallWindowType else { return }
isSwitchPosition = !isSwitchPosition
refreshListSeatItem()
viewResponder?.reloadData()
resetMiniscreen()
}
func clickVideoSeat() {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ChangeToolBarHiddenState, param: [:])
guard RoomRouter.shared.hasChatWindow() else { return }
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_HiddenChatWindow, param: [:])
}
func startPlayVideoStream(item: VideoSeatItem, renderView: UIView?) {
startPlayVideo(item: item, renderView: renderView)
}
func stopPlayVideoStream(item: VideoSeatItem) {
stopPlayVideo(item: item)
}
func updateSpeakerPlayVideoState(currentPageIndex: Int) {
guard videoSeatViewType != .speechType else { return }
if currentPageIndex == 0 {
viewResponder?.updateMiniscreen(speakerItem)
} else if let item = videoSeatItems.first(where: { $0.userId == speakerItem?.userId }),
let renderView = viewResponder?.getVideoVisibleCell(item)?.renderView {
startPlayVideo(item: item, renderView: renderView)
}
}
func stopScreenCapture() {
EngineEventCenter.shared.notifyEngineEvent(event: .onUserScreenCaptureStopped, param: [:])
engineManager.stopScreenCapture()
}
}