Files
featherVoice/TUIKit/TUIRoomKit/Source/View/Page/Widget/VideoSeat/VideoSeatView.swift
2025-08-08 10:49:36 +08:00

374 lines
14 KiB
Swift

//
// TUIVideoSeat.swift
// TUIVideoSeat
//
// Created by WesleyLei on 2022/9/13.
// Copyright © 2022 Tencent. All rights reserved.
//
import UIKit
protocol TUIVideoSeatViewResponder: AnyObject {
func switchPosition()
func clickVideoSeat()
func startPlayVideoStream(item: VideoSeatItem, renderView: UIView?)
func stopPlayVideoStream(item: VideoSeatItem)
func updateSpeakerPlayVideoState(currentPageIndex: Int)
func stopScreenCapture()
}
class TUIVideoSeatView: UIView {
private let CellID_Normal = "VideoSeatCell_Normal"
private let CellID_Mini = "VideoSeatCell_Mini"
private let viewModel: TUIVideoSeatViewModel
private var isViewReady: Bool = false
weak var responder: TUIVideoSeatViewResponder?
private var pageControl: UIPageControl = {
let control = UIPageControl()
control.currentPage = 0
control.numberOfPages = 1
control.hidesForSinglePage = true
control.isUserInteractionEnabled = false
return control
}()
init(viewModel: TUIVideoSeatViewModel) {
self.viewModel = viewModel
super.init(frame: .zero)
viewModel.viewResponder = self
responder = viewModel
isUserInteractionEnabled = true
}
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
override func layoutSubviews() {
super.layoutSubviews()
if let item = moveMiniscreen.seatItem,!moveMiniscreen.isHidden {
moveMiniscreen.updateSize(size: videoSeatLayout.getMiniscreenFrame(item: item).size)
}
let offsetYu = Int(attendeeCollectionView.contentOffset.x) % Int(attendeeCollectionView.mm_w)
let offsetMuti = CGFloat(offsetYu) / attendeeCollectionView.mm_w
let currentPage = (offsetMuti > 0.5 ? 1 : 0) + (Int(attendeeCollectionView.contentOffset.x) / Int(attendeeCollectionView.mm_w))
attendeeCollectionView.setContentOffset(
CGPoint(x: CGFloat(pageControl.currentPage) * attendeeCollectionView.frame.size.width,
y: attendeeCollectionView.contentOffset.y), animated: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var videoSeatLayout: VideoSeatLayout = {
let layout = VideoSeatLayout(viewModel: viewModel)
layout.delegate = self
return layout
}()
lazy var attendeeCollectionView: UICollectionView = {
let collection = UICollectionView(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height), collectionViewLayout:
videoSeatLayout)
collection.register(VideoSeatCell.self, forCellWithReuseIdentifier: CellID_Normal)
collection.register(TUIVideoSeatDragCell.self, forCellWithReuseIdentifier: CellID_Mini)
collection.isPagingEnabled = true
collection.showsVerticalScrollIndicator = false
collection.showsHorizontalScrollIndicator = false
collection.isUserInteractionEnabled = true
collection.contentMode = .scaleToFill
collection.backgroundColor = UIColor(0x0F1014)
if #available(iOS 11.0, *) {
collection.contentInsetAdjustmentBehavior = .never
} else {
// Fallback on earlier versions
}
if #available(iOS 10.0, *) {
collection.isPrefetchingEnabled = true
} else {
// Fallback on earlier versions
}
collection.dataSource = self
collection.delegate = self
return collection
}()
lazy var moveMiniscreen: TUIVideoSeatDragCell = {
let cell = TUIVideoSeatDragCell()
cell.frame = videoSeatLayout.getMiniscreenFrame(item: nil)
cell.isHidden = true
addSubview(cell)
return cell
}()
lazy var screenCaptureMaskView: ScreenCaptureMaskView = {
let view = ScreenCaptureMaskView(frameType: .fullScreen)
view.responder = self.responder
view.isHidden = true
return view
}()
let placeholderView: UIView = {
let view = UIView(frame: .zero)
view.isHidden = true
return view
}()
func constructViewHierarchy() {
backgroundColor = .clear
addSubview(placeholderView)
addSubview(attendeeCollectionView)
addSubview(pageControl)
addSubview(screenCaptureMaskView)
}
func activateConstraints() {
placeholderView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
attendeeCollectionView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
pageControl.snp.makeConstraints { make in
make.height.equalTo(24)
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-5)
}
screenCaptureMaskView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
func bindInteraction() {
screenCaptureMaskView.isHidden = !EngineManager.shared.store.currentUser.hasScreenStream
addGesture()
}
private func addGesture() {
let tap = UITapGestureRecognizer(target: self, action: #selector(clickVideoSeat))
addGestureRecognizer(tap)
}
@objc private func clickVideoSeat() {
responder?.clickVideoSeat()
}
func updatePageControl() {
let offsetYu = Int(attendeeCollectionView.contentOffset.x) % Int(attendeeCollectionView.mm_w)
let offsetMuti = CGFloat(offsetYu) / attendeeCollectionView.mm_w
pageControl.currentPage = (offsetMuti > 0.5 ? 1 : 0) + (Int(attendeeCollectionView.contentOffset.x) / Int(attendeeCollectionView.mm_w))
if let seatItem = moveMiniscreen.seatItem, seatItem.hasVideoStream {
if pageControl.currentPage == 0 && !moveMiniscreen.isHidden {
responder?.startPlayVideoStream(item: seatItem, renderView: moveMiniscreen.renderView)
} else {
responder?.startPlayVideoStream(item: seatItem, renderView: getVideoVisibleCell(seatItem)?.renderView)
}
}
}
deinit {
debugPrint("deinit \(self)")
}
}
// MARK: - TUIVideoSeatViewModelResponder
extension TUIVideoSeatView: TUIVideoSeatViewModelResponder {
private func freshCollectionView(block: () -> Void) {
CATransaction.begin()
CATransaction.setDisableActions(true)
block()
CATransaction.commit()
}
func reloadData() {
freshCollectionView { [weak self] in
guard let self = self else { return }
self.attendeeCollectionView.reloadData()
}
}
func insertItems(at indexPaths: [IndexPath]) {
freshCollectionView { [weak self] in
guard let self = self else { return }
let cellNumber = self.attendeeCollectionView.numberOfItems(inSection: 0)
let listSeatItemNumber = self.viewModel.listSeatItem.count
guard cellNumber + indexPaths.count == listSeatItemNumber else { return }
self.attendeeCollectionView.performBatchUpdates { [weak self] in
guard let self = self else { return }
self.attendeeCollectionView.insertItems(at: indexPaths)
}
}
}
func deleteItems(at indexPaths: [IndexPath]) {
freshCollectionView { [weak self] in
guard let self = self else { return }
var resultArray: [IndexPath] = []
let numberOfSections = self.attendeeCollectionView.numberOfSections
for indexPath in indexPaths {
let section = indexPath.section
let item = indexPath.item
guard section < numberOfSections && item < self.attendeeCollectionView.numberOfItems(inSection: section)
else { continue }
resultArray.append(indexPath)
}
let cellNumber = self.attendeeCollectionView.numberOfItems(inSection: 0)
let listSeatItemNumber = self.viewModel.listSeatItem.count
guard cellNumber - indexPaths.count == listSeatItemNumber else { return }
self.attendeeCollectionView.performBatchUpdates { [weak self] in
guard let self = self else { return }
self.attendeeCollectionView.deleteItems(at: resultArray)
}
}
}
func updateVideoSeatCellUI(_ item: VideoSeatItem) {
if let seatItem = moveMiniscreen.seatItem, seatItem.userId == item.userId {
moveMiniscreen.updateUI(item: seatItem)
}
guard let cell = getVideoVisibleCell(item) else { return }
cell.updateUI(item: item)
}
func updateSeatVolume(_ item: VideoSeatItem) {
guard let cell = getVideoVisibleCell(item) else { return }
cell.updateUIVolume(item: item)
}
func getVideoVisibleCell(_ item: VideoSeatItem) -> VideoSeatCell? {
let cellArray = attendeeCollectionView.visibleCells
guard let cell = cellArray.first(where: { cell in
if let seatCell = cell as? VideoSeatCell, seatCell.seatItem == item {
return true
} else {
return false
}
}) as? VideoSeatCell else { return nil }
return cell
}
func updateMiniscreen(_ item: VideoSeatItem?) {
guard let item = item else {
moveMiniscreen.isHidden = true
return
}
if attendeeCollectionView.contentOffset.x > 0 {
return
}
if let seatItem = moveMiniscreen.seatItem, seatItem.userId != item.userId, (getVideoVisibleCell(seatItem) == nil) {
responder?.stopPlayVideoStream(item: seatItem)
}
moveMiniscreen.updateSize(size: videoSeatLayout.getMiniscreenFrame(item: item).size)
moveMiniscreen.isHidden = false
bringSubviewToFront(moveMiniscreen)
moveMiniscreen.updateUI(item: item)
if item.isHasVideoStream {
responder?.startPlayVideoStream(item: item, renderView: moveMiniscreen.renderView)
}
}
func updateMiniscreenVolume(_ item: VideoSeatItem) {
moveMiniscreen.updateUIVolume(item: item)
}
func getMoveMiniscreen() -> TUIVideoSeatDragCell {
return moveMiniscreen
}
func showScreenCaptureMaskView(isShow: Bool) {
screenCaptureMaskView.isHidden = !isShow
if isShow {
screenCaptureMaskView.superview?.bringSubviewToFront(screenCaptureMaskView)
}
}
func destroyVideoSeatResponder() {
responder = nil
attendeeCollectionView.delegate = nil
attendeeCollectionView.dataSource = nil
}
}
// MARK: - UICollectionViewDelegateFlowLayout
extension TUIVideoSeatView: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
guard let seatItem = viewModel.listSeatItem[safe: indexPath.item] else { return }
guard let seatCell = cell as? VideoSeatCell else { return }
if seatItem.isHasVideoStream {
responder?.startPlayVideoStream(item: seatItem, renderView: seatCell.renderView)
} else {
responder?.stopPlayVideoStream(item: seatItem)
}
seatCell.updateUI(item: seatItem)
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
guard let seatCell = cell as? VideoSeatCell else { return }
if let seatItem = seatCell.seatItem {
responder?.stopPlayVideoStream(item: seatItem)
}
}
}
extension TUIVideoSeatView: UIScrollViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let currentPageIndex = Int(scrollView.contentOffset.x / scrollView.mm_w)
responder?.updateSpeakerPlayVideoState(currentPageIndex: currentPageIndex)
if currentPageIndex == 0 {
addSubview(moveMiniscreen)
} else {
attendeeCollectionView.addSubview(moveMiniscreen)
}
updatePageControl()
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
attendeeCollectionView.addSubview(moveMiniscreen)
}
}
// MARK: - UICollectionViewDataSource
extension TUIVideoSeatView: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModel.listSeatItem.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if viewModel.videoSeatViewType == .largeSmallWindowType, indexPath.row == 1 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellID_Mini, for: indexPath) as! TUIVideoSeatDragCell
cell.clickBlock = {[weak self] in
guard let self = self else { return }
self.viewModel.switchPosition()
}
return cell
} else {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: CellID_Normal,
for: indexPath) as! VideoSeatCell
return cell
}
}
}
// MARK: - UICollectionViewDataSource
extension TUIVideoSeatView: VideoSeatLayoutDelegate {
func updateNumberOfPages(numberOfPages: NSInteger) {
pageControl.numberOfPages = numberOfPages
}
}