502 lines
19 KiB
Swift
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
|
|
}
|
|
}
|