提交
This commit is contained in:
@@ -0,0 +1,501 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user