Files
featherVoice/TUIKit/TUICallKit/TUICallKit-Swift/View/Component/VideoLayout/GroupCallVideoCell.swift
2025-08-08 10:49:36 +08:00

502 lines
19 KiB
Swift

//
// GroupCallVideoCell.swift
// TUICallKit
//
// Created by vincepzhang on 2023/3/2.
//
import Foundation
import TUICallEngine
import SnapKit
class GroupCallVideoCell: UICollectionViewCell {
let selfUserStatusObserver = Observer()
let remoteUserStatusObserver = Observer()
let selfUserVideoAvailableObserver = Observer()
let remoteUserVideoAvailableObserver = Observer()
let isCameraOpenObserver = Observer()
let isMicMuteObserver = Observer()
let remotePlayoutVolumeObserver = Observer()
let selfPlayoutVolumeObserver = Observer()
let isShowLargeViewUserIdObserver = Observer()
let enableBlurBackgroundObserver = Observer()
let selfNetworkQualityObserver = Observer()
let remoteNetworkQualityObserver = Observer()
private var viewModel = GroupCallVideoCellViewModel(remote: User())
private var user: User = User()
private var isSelf: Bool = false
private var renderView = VideoView()
private let titleLabel = {
let titleLabel = UILabel(frame: CGRect.zero)
titleLabel.textColor = UIColor.t_colorWithHexString(color: "FFFFFF")
titleLabel.font = UIFont.systemFont(ofSize: 12)
titleLabel.textAlignment = TUICoreDefineConvert.getIsRTL() ? .right : .left
return titleLabel
}()
private let loadingView = {
let view = GroupLoadingView()
view.backgroundColor = .clear
return view
}()
private let avatarImageView = {
let imageView = UIImageView(frame: CGRect.zero)
imageView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill
return imageView
}()
private let micImageView = {
let imageView = UIImageView(frame: CGRect.zero)
imageView.contentMode = .scaleAspectFill
if let image = TUICallKitCommon.getBundleImage(name: "icon_mic_off") {
imageView.image = image
}
return imageView
}()
private let volumeImageView = {
let imageView = UIImageView(frame: CGRect.zero)
imageView.contentMode = .scaleAspectFill
if let image = TUICallKitCommon.getBundleImage(name: "icon_volume") {
imageView.image = image
}
return imageView
}()
private let networkQualityView = {
let imageView = UIImageView(frame: CGRect.zero)
imageView.contentMode = .scaleAspectFill
if let image = TUICallKitCommon.getBundleImage(name: "group_network_low_quality") {
imageView.image = image
}
return imageView
}()
lazy var switchCameraBtn: GroupCallVideoCustomButton = {
let btn = GroupCallVideoCustomButton(type: .system)
btn.contentMode = .scaleAspectFit
if let image = TUICallKitCommon.getBundleImage(name: "group_switch_camera") {
btn.setBackgroundImage(image, for: .normal)
}
btn.addTarget(self, action: #selector(switchCameraTouchEvent(sender: )), for: .touchUpInside)
return btn
}()
lazy var virtualBackgroundBtn: GroupCallVideoCustomButton = {
let btn = GroupCallVideoCustomButton(type: .system)
btn.contentMode = .scaleAspectFit
let imageName = TUICallState.instance.enableBlurBackground.value ? "group_virtual_background_on" : "group_virtual_background_off"
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
btn.setBackgroundImage(image, for: .normal)
}
btn.addTarget(self, action: #selector(virtualBackgroundTouchEvent(sender: )), for: .touchUpInside)
return btn
}()
override init(frame: CGRect) {
super.init(frame: frame)
constructViewHierarchy()
activateConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
if !loadingView.isHidden {
loadingView.startAnimating()
}
}
deinit {
if isSelf {
TUICallState.instance.selfUser.value.callStatus.removeObserver(selfUserStatusObserver)
TUICallState.instance.selfUser.value.videoAvailable.removeObserver(selfUserVideoAvailableObserver)
TUICallState.instance.selfUser.value.playoutVolume.removeObserver(selfPlayoutVolumeObserver)
TUICallState.instance.enableBlurBackground.removeObserver(enableBlurBackgroundObserver)
TUICallState.instance.selfUser.value.networkQualityReminder.removeObserver(selfNetworkQualityObserver)
} else {
viewModel.remoteUserStatus.removeObserver(remoteUserStatusObserver)
viewModel.remoteUserVideoAvailable.removeObserver(remoteUserVideoAvailableObserver)
viewModel.remoteIsShowLowNetworkQuality.removeObserver(remoteNetworkQualityObserver)
if TUICallState.instance.showVirtualBackgroundButton {
viewModel.remoteUserVolume.removeObserver(remotePlayoutVolumeObserver)
}
}
TUICallState.instance.showLargeViewUserId.removeObserver(isMicMuteObserver)
for view in contentView.subviews {
view.removeFromSuperview()
}
}
// MARK: UI Specification Processing
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
if isViewReady { return }
constructViewHierarchy()
activateConstraints()
isViewReady = true
if !loadingView.isHidden {
loadingView.startAnimating()
}
}
func constructViewHierarchy() {
contentView.addSubview(renderView)
contentView.addSubview(avatarImageView)
contentView.addSubview(loadingView)
contentView.addSubview(titleLabel)
contentView.addSubview(micImageView)
contentView.addSubview(volumeImageView)
contentView.addSubview(networkQualityView)
contentView.addSubview(switchCameraBtn)
if TUICallState.instance.showVirtualBackgroundButton {
contentView.addSubview(virtualBackgroundBtn)
}
}
func activateConstraints() {
renderView.snp.makeConstraints { make in
make.edges.equalTo(self.contentView)
}
avatarImageView.snp.makeConstraints { make in
make.edges.equalTo(self.contentView)
}
loadingView.snp.makeConstraints { make in
make.center.equalTo(self)
make.width.height.equalTo(40.scaleWidth())
}
if viewModel.isShowLargeViewUserId.value {
activateLargeViewConstraints()
} else {
activateSmallViewConstraints()
}
}
func activateLargeViewConstraints() {
titleLabel.snp.remakeConstraints { make in
make.leading.equalTo(self.contentView).offset(8.scaleWidth())
make.height.equalTo(24.scaleWidth())
make.bottom.equalTo(self.contentView).offset(-8.scaleWidth())
}
micImageView.snp.remakeConstraints { make in
make.width.height.equalTo(24.scaleWidth())
make.leading.equalTo(titleLabel.snp.trailing).offset(8.scaleWidth())
make.bottom.equalTo(self.contentView).offset(-8.scaleWidth())
}
volumeImageView.snp.remakeConstraints { make in
make.width.height.equalTo(24.scaleWidth())
make.leading.equalTo(titleLabel.snp.trailing).offset(8.scaleWidth())
make.bottom.equalTo(self.contentView).offset(-8.scaleWidth())
}
if TUICallState.instance.showVirtualBackgroundButton {
virtualBackgroundBtn.snp.remakeConstraints { make in
make.width.height.equalTo(24.scaleWidth())
make.bottom.trailing.equalTo(self.contentView).offset(-8.scaleWidth())
}
switchCameraBtn.snp.remakeConstraints { make in
make.width.height.equalTo(24.scaleWidth())
make.centerY.equalTo(virtualBackgroundBtn)
make.trailing.equalTo(virtualBackgroundBtn.snp.leading).offset(-10.scaleWidth())
}
} else {
switchCameraBtn.snp.remakeConstraints { make in
make.width.height.equalTo(24.scaleWidth())
make.bottom.trailing.equalTo(self.contentView).offset(-8.scaleWidth())
}
}
networkQualityView.snp.remakeConstraints { make in
make.width.height.equalTo(24.scaleWidth())
make.centerY.equalTo(switchCameraBtn)
make.trailing.equalTo(switchCameraBtn.snp.leading).offset(-10.scaleWidth())
}
}
func activateSmallViewConstraints() {
volumeImageView.snp.remakeConstraints { make in
make.width.height.equalTo(24.scaleWidth())
make.leading.equalTo(self.contentView).offset(8.scaleWidth())
make.bottom.equalTo(self.contentView).offset(-8.scaleWidth())
}
networkQualityView.snp.remakeConstraints { make in
make.width.height.equalTo(24.scaleWidth())
if self.micImageView.isHidden {
make.trailing.equalTo(self.contentView).offset(-8.scaleWidth())
} else {
make.trailing.equalTo(self.micImageView.snp.leading).offset(-8.scaleWidth())
}
make.bottom.equalTo(self.contentView).offset(-8.scaleWidth())
}
micImageView.snp.remakeConstraints { make in
make.width.height.equalTo(24.scaleWidth())
make.trailing.equalTo(self.contentView).offset(-8.scaleWidth())
make.bottom.equalTo(self.contentView).offset(-8.scaleWidth())
}
}
func initCell(user: User) {
self.user = user
viewModel = GroupCallVideoCellViewModel(remote: user)
isSelf = TUICallState.instance.selfUser.value.id.value == user.id.value ? true : false
initWaitingUI()
registerObserveState()
}
// MARK: Action Event
@objc func switchCameraTouchEvent(sender: UIButton) {
CallEngineManager.instance.switchCamera()
}
@objc func virtualBackgroundTouchEvent(sender: UIButton ) {
CallEngineManager.instance.setBlurBackground()
}
// MARK: Register TUICallState Observer && Update UI
func registerObserveState() {
callStatusChanged()
videoAvailableChanged()
isCameraOpenChanged()
volumeChanged()
isShowLargeViewUserIdChanged()
isMicMuteChanged()
enableBlurBackgroundChanged()
networkQualityChanged()
}
func callStatusChanged() {
if isSelf {
TUICallState.instance.selfUser.value.callStatus.addObserver(selfUserStatusObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateSelfUserUI()
})
} else {
viewModel.remoteUserStatus.addObserver(remoteUserStatusObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateRemoteUserCellUI()
})
}
}
func videoAvailableChanged() {
if isSelf {
TUICallState.instance.selfUser.value.videoAvailable.addObserver(selfUserVideoAvailableObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateSelfUserUI()
})
} else {
viewModel.remoteUserVideoAvailable.addObserver(remoteUserVideoAvailableObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateRemoteUserCellUI()
})
}
}
func volumeChanged() {
if isSelf {
TUICallState.instance.selfUser.value.playoutVolume.addObserver(selfPlayoutVolumeObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.volumeImageView.isHidden = newValue == 0 ? true : false
})
} else {
viewModel.remoteUserVolume.addObserver(remotePlayoutVolumeObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.volumeImageView.isHidden = newValue == 0 ? true : false
})
}
}
func isCameraOpenChanged() {
if isSelf {
TUICallState.instance.isCameraOpen.addObserver(isCameraOpenObserver) { [weak self] newValue, _ in
guard let self = self else { return }
self.updateSelfUserUI()
}
}
}
func isShowLargeViewUserIdChanged() {
viewModel.isShowLargeViewUserId.addObserver(isShowLargeViewUserIdObserver) { [weak self] newValue, _ in
guard let self = self else { return }
if newValue {
self.updateLargeViewUI()
} else {
self.updateSmallViewUI()
}
}
}
func isMicMuteChanged() {
if isSelf {
TUICallState.instance.isMicMute.addObserver(isMicMuteObserver) { [weak self] newValue, _ in
guard let self = self else { return }
self.micImageView.isHidden = !newValue
}
}
}
func enableBlurBackgroundChanged () {
if isSelf && TUICallState.instance.showVirtualBackgroundButton {
TUICallState.instance.enableBlurBackground.addObserver(enableBlurBackgroundObserver) { [weak self] newValue, _ in
guard let self = self else { return }
let imageName = TUICallState.instance.enableBlurBackground.value ? "group_virtual_background_on" : "group_virtual_background_off"
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
self.virtualBackgroundBtn.setBackgroundImage(image, for: .normal)
}
}
}
}
func networkQualityChanged () {
if isSelf {
TUICallState.instance.selfUser.value.networkQualityReminder.addObserver(selfNetworkQualityObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.networkQualityView.isHidden = !newValue
})
} else {
viewModel.remoteIsShowLowNetworkQuality.addObserver(remoteNetworkQualityObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.networkQualityView.isHidden = !newValue
})
}
}
func updateLargeViewUI() {
if isSelf {
switchCameraBtn.isHidden = !TUICallState.instance.isCameraOpen.value
virtualBackgroundBtn.isHidden = !TUICallState.instance.isCameraOpen.value || !TUICallState.instance.showVirtualBackgroundButton
titleLabel.isHidden = false
micImageView.isHidden = !TUICallState.instance.isMicMute.value
} else {
switchCameraBtn.isHidden = true
virtualBackgroundBtn.isHidden = true
titleLabel.isHidden = false
micImageView.isHidden = true
}
activateLargeViewConstraints()
}
func updateSmallViewUI() {
switchCameraBtn.isHidden = true
virtualBackgroundBtn.isHidden = true
titleLabel.isHidden = true
if isSelf {
micImageView.isHidden = !TUICallState.instance.isMicMute.value
} else {
micImageView.isHidden = true
}
activateSmallViewConstraints()
}
// MARK: Update UI
func initWaitingUI() {
titleLabel.text = User.getUserDisplayName(user: viewModel.remoteUser)
setUserAvatar()
if isSelf {
initSelfUserUI()
} else {
updateRemoteUserCellUI()
}
}
func initSelfUserUI() {
hiddenAllSubView()
micImageView.isHidden = !TUICallState.instance.isMicMute.value
if !VideoFactory.instance.isExistVideoView(videoView: renderView) {
renderView = VideoFactory.instance.createVideoView(userId: TUICallState.instance.selfUser.value.id.value, frame: CGRect.zero)
renderView.isUserInteractionEnabled = false
}
if TUICallState.instance.isCameraOpen.value == true {
renderView.isHidden = false
switchCameraBtn.isHidden = false
CallEngineManager.instance.openCamera(videoView: renderView)
} else {
renderView.isHidden = true
avatarImageView.isHidden = false
}
}
func updateSelfUserUI() {
hiddenAllSubView()
micImageView.isHidden = !TUICallState.instance.isMicMute.value
if TUICallState.instance.isCameraOpen.value == true {
renderView.isHidden = false
switchCameraBtn.isHidden = false
virtualBackgroundBtn.isHidden = !TUICallState.instance.showVirtualBackgroundButton
} else {
switchCameraBtn.isHidden = true
virtualBackgroundBtn.isHidden = true
avatarImageView.isHidden = false
}
}
func updateRemoteUserCellUI() {
hiddenAllSubView()
if viewModel.remoteUserStatus.value == .waiting {
loadingView.isHidden = false
loadingView.startAnimating()
avatarImageView.isHidden = false
} else if viewModel.remoteUserStatus.value == .accept {
CallEngineManager.instance.startRemoteView(user: viewModel.remoteUser, videoView: renderView)
if viewModel.remoteUserVideoAvailable.value == true {
renderView.isUserInteractionEnabled = false
renderView.isHidden = false
} else {
avatarImageView.isHidden = false
}
}
}
func hiddenAllSubView() {
renderView.isHidden = true
loadingView.isHidden = true
loadingView.stopAnimating()
avatarImageView.isHidden = true
micImageView.isHidden = true
volumeImageView.isHidden = true
switchCameraBtn.isHidden = true
virtualBackgroundBtn.isHidden = true
networkQualityView.isHidden = true
}
// MARK: Private Method
func setUserAvatar() {
let userIcon: UIImage? = TUICallKitCommon.getBundleImage(name: "default_user_icon")
if viewModel.remoteUser.avatar.value == "" {
guard let image = userIcon else { return }
avatarImageView.image = image
} else {
avatarImageView.sd_setImage(with: URL(string: viewModel.remoteUser.avatar.value), placeholderImage: userIcon)
}
}
}
class GroupCallVideoCustomButton: UIButton {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let expandedBounds = bounds.insetBy(dx: -4.scaleWidth(), dy: -4.scaleWidth())
return expandedBounds.contains(point) ? self : nil
}
}