diff --git a/EaseCallUIKit.podspec b/EaseCallUIKit.podspec index c5b12a0..ab21342 100644 --- a/EaseCallUIKit.podspec +++ b/EaseCallUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'EaseCallUIKit' - s.version = '4.16.0' + s.version = '4.17.0' s.summary = 'A short description of EaseCallUIKit.' # This description is used to generate tags and improve search results. @@ -42,6 +42,6 @@ TODO: Add long description of the pod here. # s.public_header_files = 'Pod/Classes/**/*.h' s.frameworks = 'UIKit', 'Foundation', 'Combine', 'AudioToolbox', 'AVFoundation','AVKit', 'CoreMedia', 'CoreVideo', 'CoreGraphics' - s.dependency 'HyphenateChat','>= 4.16.0' - s.dependency 'AgoraRtcEngine_iOS/RtcBasic', '~> 4.5.0' + s.dependency 'HyphenateChat','>= 4.17.0' + s.dependency 'AgoraRtcEngine_iOS/RtcBasic', '~> 4.6.0' end diff --git a/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager+RTC.swift b/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager+RTC.swift index e432e4d..935d912 100644 --- a/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager+RTC.swift +++ b/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager+RTC.swift @@ -638,24 +638,16 @@ extension CallKitManager: AgoraRtcEngineDelegate { } public func rtcEngine(_ engine: AgoraRtcEngineKit, reportAudioVolumeIndicationOfSpeakers speakers: [AgoraRtcAudioVolumeInfo], totalVolume: Int) {//On audio volume indication change of speakers - if let call = self.callInfo { - if call.type == .groupCall {// Only handle audio volume indication in multi call - var speakerInfos = [UInt:UInt]() - for speaker in speakers { - speakerInfos[speaker.uid] = speaker.volume - } - let uids = speakers.map { NSNumber(value: $0.uid != 0 ? UInt32($0.uid):self.currentUserRTCUID) } - ChatClient.shared().getUserId(byRTCUIds: uids) { [weak self] relations, error in - guard let `self` = self else { return } - if error == nil { - let relationships = relations ?? [:] - for ship in relationships { - if let streamView = self.canvasCache[ship.value],streamView.item.uid == UInt32(truncating: ship.key) { - streamView.updateAudioVolume(speakerInfos[UInt(streamView.item.uid)] ?? 0) + DispatchQueue.main.async { + if let call = self.callInfo { + if call.type == .groupCall {// Only handle audio volume indication in multi call + for speaker in speakers { + if let item = self.itemsCache.values.first(where: { $0.uid == speaker.uid }) { + let streamView = self.canvasCache[item.userId] + if item.uid == speaker.uid { + streamView?.updateAudioVolume(speaker.volume) } } - } else { - consoleLogInfo("Failed to get userId by RTC UIDs: \(error?.errorDescription ?? "Unknown error")", type: .error) } } } diff --git a/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager.swift b/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager.swift index 117a4d2..4b71279 100644 --- a/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager.swift +++ b/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager.swift @@ -166,7 +166,7 @@ public let CallKitVersion = "1.0.0" } self.engine?.enableAudio() self.engine?.enable(inEarMonitoring: true) - self.engine?.enableAudioVolumeIndication(618, smooth: 10, reportVad: true) + self.engine?.enableAudioVolumeIndication(618, smooth: 5, reportVad: true) self.engine?.setDefaultAudioRouteToSpeakerphone(true) self.engine?.setVideoFrameDelegate(self) return nil diff --git a/Sources/EaseCallUIKit/Classes/Resources/CallResource.bundle/already_seleted@2x.png b/Sources/EaseCallUIKit/Classes/Resources/CallResource.bundle/already_seleted@2x.png new file mode 100644 index 0000000..cf775f9 Binary files /dev/null and b/Sources/EaseCallUIKit/Classes/Resources/CallResource.bundle/already_seleted@2x.png differ diff --git a/Sources/EaseCallUIKit/Classes/Resources/CallResource.bundle/already_seleted@3x.png b/Sources/EaseCallUIKit/Classes/Resources/CallResource.bundle/already_seleted@3x.png new file mode 100644 index 0000000..a13ba6b Binary files /dev/null and b/Sources/EaseCallUIKit/Classes/Resources/CallResource.bundle/already_seleted@3x.png differ diff --git a/Sources/EaseCallUIKit/Classes/UI/Cells/GroupParticipantsCell.swift b/Sources/EaseCallUIKit/Classes/UI/Cells/GroupParticipantsCell.swift index cb9103c..be54c69 100644 --- a/Sources/EaseCallUIKit/Classes/UI/Cells/GroupParticipantsCell.swift +++ b/Sources/EaseCallUIKit/Classes/UI/Cells/GroupParticipantsCell.swift @@ -48,7 +48,11 @@ import UIKit self.nickName.text = nickName self.nickName.font = UIFont.callTheme.labelLarge self.avatar.image(with: profile.avatarURL, placeHolder: CallAppearance.avatarPlaceHolder) - self.checkbox.image = UIImage(named: profile.selected ? "select":"unselect", in: .callBundle, compatibleWith: nil) + if let user = CallKitManager.shared.itemsCache[profile.id] { + self.checkbox.image = UIImage(named: "already_seleted", in: .callBundle, compatibleWith: nil) + } else { + self.checkbox.image = UIImage(named: profile.selected ? "select":"unselect", in: .callBundle, compatibleWith: nil) + } self.separatorLine.backgroundColor = Theme.style == .dark ? UIColor.callTheme.neutralColor2:UIColor.callTheme.neutralColor9 } diff --git a/Sources/EaseCallUIKit/Classes/UI/Controllers/Call1v1VideoViewController.swift b/Sources/EaseCallUIKit/Classes/UI/Controllers/Call1v1VideoViewController.swift index 534c28e..f805db1 100644 --- a/Sources/EaseCallUIKit/Classes/UI/Controllers/Call1v1VideoViewController.swift +++ b/Sources/EaseCallUIKit/Classes/UI/Controllers/Call1v1VideoViewController.swift @@ -674,6 +674,7 @@ extension Call1v1VideoViewController: AVPictureInPictureControllerDelegate { UIApplication.shared.call.keyWindow?.rootViewController?.present(existingVC, animated: true) { // 将floatView恢复到existingVC existingVC.ensureFloatViewVisible() + existingVC.floatViewClicked(dragView: existingVC.floatView) completionHandler(true) } } else { @@ -683,6 +684,7 @@ extension Call1v1VideoViewController: AVPictureInPictureControllerDelegate { UIApplication.shared.call.keyWindow?.rootViewController?.present(self, animated: true) { // 恢复floatView self.ensureFloatViewVisible() + self.floatViewClicked(dragView: self.floatView) completionHandler(true) } } diff --git a/Sources/EaseCallUIKit/Classes/UI/Controllers/MultiCallParticipantsController.swift b/Sources/EaseCallUIKit/Classes/UI/Controllers/MultiCallParticipantsController.swift index 830609c..5f0d112 100644 --- a/Sources/EaseCallUIKit/Classes/UI/Controllers/MultiCallParticipantsController.swift +++ b/Sources/EaseCallUIKit/Classes/UI/Controllers/MultiCallParticipantsController.swift @@ -162,7 +162,6 @@ import UIKit })) } self.participants.removeAll { $0.id == ChatClient.shared().currentUsername ?? "" } - self.participants.removeAll { self.excludeUsers.contains($0.id) } DispatchQueue.main.async { self.participantsList.reloadData() } @@ -202,6 +201,9 @@ extension MultiCallParticipantsController: UITableViewDelegate,UITableViewDataSo @objc open func didSelectRowAt(indexPath: IndexPath) { if let profile = self.participants[safely: indexPath.row] { + if let user = CallKitManager.shared.itemsCache[profile.id] { + return + } profile.selected = !profile.selected self.participantsList.reloadData() } diff --git a/Sources/EaseCallUIKit/Classes/UI/Views/Call1v1BottomView.swift b/Sources/EaseCallUIKit/Classes/UI/Views/Call1v1BottomView.swift index 21e99bc..b46a211 100644 --- a/Sources/EaseCallUIKit/Classes/UI/Views/Call1v1BottomView.swift +++ b/Sources/EaseCallUIKit/Classes/UI/Views/Call1v1BottomView.swift @@ -371,7 +371,7 @@ public class Call1v1BottomView: UIView { // MARK: - Actions private func handleButtonTap(_ sender: CallButtonView) { // Haptic feedback - UIImpactFeedbackGenerator.impactOccurred(style: .light) + UIImpactFeedbackGenerator.impactOccurred(style: .medium) print("tag: \(sender.buttonTag)") guard let data = sender.data, let buttonType = self.getActionType(for: data, button: sender) else { return } diff --git a/Sources/EaseCallUIKit/Classes/UI/Views/CallStreamView.swift b/Sources/EaseCallUIKit/Classes/UI/Views/CallStreamView.swift index 8636553..28e726b 100644 --- a/Sources/EaseCallUIKit/Classes/UI/Views/CallStreamView.swift +++ b/Sources/EaseCallUIKit/Classes/UI/Views/CallStreamView.swift @@ -21,7 +21,7 @@ public class CallStreamView: UIImageView { private let loadingView = UIImageView().contentMode(.scaleAspectFit) public let userInfoView = UserInfoView() - var displayMode: UserInfoDisplayMode = .all { + var displayMode: UserInfoDisplayMode = .nameOnly { didSet { userInfoView.displayMode = displayMode if self.item.isExpanded { @@ -215,7 +215,26 @@ public class CallStreamView: UIImageView { } func updateAudioVolume(_ volume: UInt) { - self.userInfoView.isSpeaking = volume > 10 // Adjust threshold as needed + if self.item.waiting { + return + } + // 设置说话状态 + self.userInfoView.isSpeaking = volume > 0 + + // 根据音量动态调整显示模式 + if volume > 0 && !self.item.audioMuted { + // 有音量且未静音时,显示音频图标 + if self.userInfoView.displayMode == .nameOnly { + self.userInfoView.displayMode = .all + } + } else if volume == 0 && !self.item.audioMuted { + // 无音量且未静音时,隐藏音频图标(仅显示昵称) + if self.userInfoView.displayMode == .all { + self.userInfoView.displayMode = .nameOnly + } + } + + // 清理覆盖视图 coverView.isHidden = true coverView.removeFromSuperview() } @@ -289,6 +308,15 @@ public class CallStreamItem: NSObject { } } +// MARK: - User Info Component +// MARK: - Display Mode +public enum UserInfoDisplayMode { + case all // 显示昵称和按钮 + case nameOnly // 只显示昵称 + case buttonsOnly // 只显示按钮 + case hidden // 完全隐藏 +} + // MARK: - User Info Component public class UserInfoView: UIView { @@ -300,7 +328,7 @@ public class UserInfoView: UIView { private var containerTrailingConstraint: NSLayoutConstraint? private var nicknameLabelWidthConstraint: NSLayoutConstraint? - var displayMode: UserInfoDisplayMode = .all { + var displayMode: UserInfoDisplayMode = .nameOnly { didSet { updateDisplayMode() } @@ -318,13 +346,35 @@ public class UserInfoView: UIView { } } - var isSpeaking: Bool = false { + @MainActor var isSpeaking: Bool = false { didSet { - if isSpeaking,!isAudioMuted { + if isSpeaking && !isAudioMuted { + // 正在说话且未静音时显示说话图标 audioButton.setImage(UIImage(named: "speaking", in: .callBundle, with: nil), for: .normal) + audioButton.isHidden = false + + // 动态更新约束以显示音频按钮 + containerStackView.spacing = 6 + setNeedsUpdateConstraints() + + } else if !isSpeaking && !isAudioMuted { + // 不说话且未静音时清空图标 + audioButton.setImage(nil, for: .normal) + audioButton.isHidden = true + + // 动态更新约束以隐藏音频按钮 + containerStackView.spacing = 0 + setNeedsUpdateConstraints() + } else { + // 静音状态保持原有逻辑 updateAudioButton() } + + // 触发布局更新 + invalidateIntrinsicContentSize() + setNeedsLayout() + layoutIfNeeded() } } @@ -405,7 +455,7 @@ public class UserInfoView: UIView { if isAudioMuted { audioButton.setImage(UIImage(systemName: "mic.slash.fill"), for: .normal) } else { - audioButton.setImage(UIImage(systemName: "mic.fill"), for: .normal) + audioButton.setImage(nil, for: .normal) } } @@ -421,6 +471,16 @@ public class UserInfoView: UIView { containerStackView.spacing = 6 nicknameLabelWidthConstraint?.constant = 80 + case .nameOnly: + // 只显示昵称,隐藏音频按钮 + nicknameLabel.isHidden = false + audioButton.isHidden = true + self.isHidden = false + + // 移除间距,因为没有按钮 + containerStackView.spacing = 0 + nicknameLabelWidthConstraint?.constant = 80 + case .buttonsOnly: // 只显示按钮 nicknameLabel.isHidden = true @@ -438,7 +498,6 @@ public class UserInfoView: UIView { // 触发布局更新 setNeedsLayout() layoutIfNeeded() - } // 重写 intrinsicContentSize 以支持自动布局 @@ -460,13 +519,6 @@ public class UserInfoView: UIView { } } -// MARK: - Display Mode -public enum UserInfoDisplayMode { - case all // 显示昵称和按钮 - case buttonsOnly // 只显示按钮 - case hidden // 完全隐藏 -} - // MARK: - 便利方法 extension UserInfoView { @@ -485,6 +537,11 @@ extension UserInfoView { let nicknameWidth = min(nicknameLabel.intrinsicContentSize.width, 80) return 6 + nicknameWidth + 6 + 16 + 6 // padding + nickname + spacing + button + padding + case .nameOnly: + // 只有昵称,没有音频按钮 + let nicknameWidth = min(nicknameLabel.intrinsicContentSize.width, 80) + return 6 + nicknameWidth + 6 // padding + nickname + padding + case .buttonsOnly: return 6 + 16 + 6 // padding + button + padding @@ -505,21 +562,3 @@ extension UserInfoView { }) } } - -// MARK: - 调试辅助 -#if DEBUG -extension UserInfoView { - - /// 添加边框以便调试 - public func enableDebugMode() { - layer.borderWidth = 1 - layer.borderColor = UIColor.red.cgColor - - nicknameLabel.layer.borderWidth = 0.5 - nicknameLabel.layer.borderColor = UIColor.green.cgColor - - audioButton.layer.borderWidth = 0.5 - audioButton.layer.borderColor = UIColor.blue.cgColor - } -} -#endif diff --git a/Sources/EaseCallUIKit/Classes/UI/Views/MultiCallBottomView.swift b/Sources/EaseCallUIKit/Classes/UI/Views/MultiCallBottomView.swift index 37c4366..4635ce8 100644 --- a/Sources/EaseCallUIKit/Classes/UI/Views/MultiCallBottomView.swift +++ b/Sources/EaseCallUIKit/Classes/UI/Views/MultiCallBottomView.swift @@ -35,6 +35,7 @@ public class MultiCallBottomView: UIView { public var didTapButton: ((CallButtonType) -> Void)? public var animationToExpand: (() -> Void)? + // 添加属性来控制按钮的启用状态 public var isCallConnected: Bool = false { didSet { @@ -42,17 +43,34 @@ public class MultiCallBottomView: UIView { } } + // 添加属性来跟踪摄像头状态 + private var isCameraOn: Bool = true { + didSet { + updateFlipButtonState() + } + } + // 添加需要在通话接通后才能使用的按钮索引 private let requiresConnectionButtonIndexes = [3] // Camera 按钮 + // 更新Flip按钮状态(基于Camera状态) + private func updateFlipButtonState() { + guard buttonViews.count > 0 else { return } + let flipButton = buttonViews[0] // Flip按钮是第一个 +// flipButton.isUserInteractionEnabled = isCameraOn + flipButton.alpha = isCameraOn ? 1.0 : 0.5 + } + // 更新按钮交互状态 private func updateButtonsInteractionState() { for (index, buttonView) in buttonViews.enumerated() { if requiresConnectionButtonIndexes.contains(index) { - buttonView.isUserInteractionEnabled = isCallConnected +// buttonView.isUserInteractionEnabled = isCallConnected buttonView.alpha = isCallConnected ? 1.0 : 0.5 // 视觉提示 } } + // 同时更新Flip按钮状态 + updateFlipButtonState() } override init(frame: CGRect) { @@ -129,6 +147,12 @@ public class MultiCallBottomView: UIView { buttonView.didTap = { [weak self] button in guard let self = self else { return } + // 检查Flip按钮是否因Camera关闭而不可用 + if button.tag == 0 && !self.isCameraOn { + self.shakeButton(button) + return + } + // 检查按钮是否需要通话接通 if self.requiresConnectionButtonIndexes.contains(button.tag) && !self.isCallConnected { // 提供视觉反馈但不执行操作 @@ -136,10 +160,16 @@ public class MultiCallBottomView: UIView { return } - UIImpactFeedbackGenerator.impactOccurred(style: .light) + UIImpactFeedbackGenerator.impactOccurred(style: .medium) if let buttonData = button.data { buttonData.isSelected.toggle() buttonView.configure(data: buttonData) + + // 如果是Camera按钮,更新摄像头状态 + if button.tag == 3 { + self.isCameraOn = !buttonData.isSelected // isSelected为true表示camera_off状态 + } + if let buttonType = self.getActionType(for: buttonData, button: button) { self.didTapButton?(buttonType) } @@ -149,10 +179,14 @@ public class MultiCallBottomView: UIView { addSubview(buttonView) buttonViews.append(buttonView) } + + // 初始化Flip按钮状态 + updateFlipButtonState() } // 添加摇晃动画提示按钮不可用 private func shakeButton(_ button: UIView) { + UIImpactFeedbackGenerator.impactOccurred(style: .medium) let animation = CAKeyframeAnimation(keyPath: "transform.translation.x") animation.timingFunction = CAMediaTimingFunction(name: .linear) animation.duration = 0.4 @@ -376,11 +410,16 @@ public class MultiCallBottomView: UIView { if let data = buttonView.data { data.isSelected.toggle() buttonView.configure(data: data) + + // 如果是Camera按钮,更新摄像头状态 + if selectedIndex == 3 { + isCameraOn = !data.isSelected + } + // 执行对应的操作 if let actionType = getActionType(for: data, button: buttonView) { didTapButton?(actionType) } } - } } diff --git a/Sources/EaseCallUIKit/Classes/UI/Views/MultiPersonCallView.swift b/Sources/EaseCallUIKit/Classes/UI/Views/MultiPersonCallView.swift index 2dc13b4..e0bc1fc 100644 --- a/Sources/EaseCallUIKit/Classes/UI/Views/MultiPersonCallView.swift +++ b/Sources/EaseCallUIKit/Classes/UI/Views/MultiPersonCallView.swift @@ -328,12 +328,12 @@ public class MultiPersonCallView: UIView { } else { for view in itemViews { if totalCount > 6 { - view.displayMode = .buttonsOnly + view.displayMode = view.item.waiting ? .nameOnly:.buttonsOnly } else { if ScreenHeight/ScreenWidth > 1.8,totalCount <= 4 { - view.displayMode = .all + view.displayMode = view.item.waiting ? .nameOnly:.all } else { - view.displayMode = .buttonsOnly + view.displayMode = view.item.waiting ? .nameOnly:.buttonsOnly } } }