From 81273e8015b6f47a515c7f6b038281b5c05ecbb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E7=BB=A7=E8=B6=85?= <984065974@qq.com> Date: Mon, 3 Nov 2025 16:54:53 +0800 Subject: [PATCH 1/3] HIM-19562 --- .../Implements/CallKitManager+RTC.swift | 25 +++- .../Implements/CallKitManager+Signaling.swift | 16 ++- .../Call1v1VideoViewController.swift | 14 +-- .../Controllers/CallMultiViewController.swift | 119 +++++++++++++++--- .../UI/Views/MultiCallBottomView.swift | 18 +-- 5 files changed, 144 insertions(+), 48 deletions(-) diff --git a/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager+RTC.swift b/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager+RTC.swift index 935d912..dd367fa 100644 --- a/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager+RTC.swift +++ b/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager+RTC.swift @@ -604,14 +604,11 @@ extension CallKitManager: AgoraRtcEngineDelegate { controller.callView.micView.isHidden = true if self.isVideoExchanged {// If video is exchanged, update mic view visibility if muted { - controller.micView.isHidden = false controller.floatView.updateAudioState(!muted) } else { - controller.micView.isHidden = true controller.floatView.updateAudioState(muted) } } else { - controller.micView.isHidden = true controller.floatView.updateAudioState(muted) } } else {// If current controller is not Call1v1VideoViewController @@ -619,14 +616,11 @@ extension CallKitManager: AgoraRtcEngineDelegate { controller.callView.micView.isHidden = true if self.isVideoExchanged {// If video is exchanged, update mic view visibility and audio state if muted { - controller.micView.isHidden = false controller.floatView.updateAudioState(!muted) } else { - controller.micView.isHidden = true controller.floatView.updateAudioState(muted) } } else {// If video is not exchanged, hide mic view and update audio state - controller.micView.isHidden = true controller.floatView.updateAudioState(muted) } } @@ -748,6 +742,25 @@ extension CallKitManager: AgoraRtcEngineDelegate { extension CallKitManager: AgoraVideoFrameDelegate { public func onCapture(_ videoFrame: AgoraOutputVideoFrame, sourceType: AgoraVideoSourceType) -> Bool {// This method is called when local video frame is captured. if let call = self.callInfo { + // 处理群组通话预览(仅前台且当前显示的页面) + if call.type == .groupCall { + // 只处理当前正在显示的 CallMultiViewController + if let controller = UIViewController.currentController as? CallMultiViewController { + // 未连接状态且开启了摄像头预览 + if controller.isCameraPreviewEnabled, let previewView = controller.localPreviewView { + if let pixelBuffer = videoFrame.pixelBuffer { + previewView.renderVideoPixelBuffer(pixelBuffer: pixelBuffer, width: videoFrame.width, height: videoFrame.height) + } else { + previewView.renderFromVideoFrameData(videoData: videoFrame) + } + return true + } + } + // 群组通话在后台或缩小时不处理预览,直接返回 + return true + } + + // 原有逻辑:处理1v1视频通话 if call.type == .singleVideo { if let controller = UIViewController.currentController as? Call1v1VideoViewController { if let pixelBuffer = videoFrame.pixelBuffer { diff --git a/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager+Signaling.swift b/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager+Signaling.swift index 5053c3f..271cc80 100644 --- a/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager+Signaling.swift +++ b/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager+Signaling.swift @@ -1446,11 +1446,19 @@ extension CallKitManager: CallMessageService { public func accept() { AudioPlayerManager.shared.stopAudio() if let call = self.callInfo { - if call.type == .singleVideo { - self.setupLocalVideo() -// self.enableLocalVideo(true) - } else { + switch call.type { + case .singleAudio: self.enableLocalVideo(false) + case .singleVideo: + self.setupLocalVideo() + case .groupCall: + if let vc = UIViewController.currentController as? CallMultiViewController { + if vc.isCameraPreviewEnabled { + self.setupLocalVideo() + } else { + self.enableLocalVideo(false) + } + } } self.engine?.enableAudio() self.enableLocalAudio(true) diff --git a/Sources/EaseCallUIKit/Classes/UI/Controllers/Call1v1VideoViewController.swift b/Sources/EaseCallUIKit/Classes/UI/Controllers/Call1v1VideoViewController.swift index f805db1..3c315eb 100644 --- a/Sources/EaseCallUIKit/Classes/UI/Controllers/Call1v1VideoViewController.swift +++ b/Sources/EaseCallUIKit/Classes/UI/Controllers/Call1v1VideoViewController.swift @@ -83,10 +83,6 @@ open class Call1v1VideoViewController: UIViewController { return drag }() - public lazy var micView: UIImageView = { - UIImageView(frame: CGRect(x: 18, y: self.bottomView.frame.minY - 14, width: 14, height: 14)).image(UIImage(named: "mic_off", in: .callBundle, with: nil)).isUserInteractionEnabled(false).tag(1002) - }() - public private(set) var role: CallRole = .caller /// Picture-in-Picture controller @@ -193,9 +189,8 @@ open class Call1v1VideoViewController: UIViewController { // 新增:集中管理视图层级 private func setupViews() { - // 确保视图层级正确 - self.micView.isHidden = true - self.view.addSubViews([self.background, self.navigationBar, self.bottomView, self.micView,self.navigationBlur]) + + self.view.addSubViews([self.background, self.navigationBar, self.bottomView,self.navigationBlur]) self.background.addSubViews([self.callView, self.floatView]) self.navigationBlur.image = UIImage(named: "mask", in: .callBundle, with: nil) // 确保floatView在最上层 @@ -217,9 +212,7 @@ open class Call1v1VideoViewController: UIViewController { self.background.bringSubviewToFront(self.floatView) self.floatView.isUserInteractionEnabled = true self.callView.isUserInteractionEnabled = false - self.floatView.micView.isHidden = false self.callView.micView.isHidden = true - self.micView.isHidden = true self.floatView.updateAudioState(self.floatView.isAudioMuted) self.floatView.blurEffectView.isHidden = true self.callView.blurEffectView.isHidden = false @@ -241,7 +234,6 @@ open class Call1v1VideoViewController: UIViewController { self.callView.isUserInteractionEnabled = true self.floatView.micView.isHidden = true self.callView.micView.isHidden = true - self.micView.isHidden = !self.floatView.isAudioMuted self.floatView.blurEffectView.isHidden = false self.callView.blurEffectView.isHidden = true } @@ -490,13 +482,11 @@ open class Call1v1VideoViewController: UIViewController { UIView.animate(withDuration: 0.3) { self.navigationBar.alpha = 1 self.bottomView.alpha = 1 - self.micView.frame = CGRect(x: 18, y: self.bottomView.frame.minY - 14 , width: 14, height: 14) } } else { UIView.animate(withDuration: 0.3) { self.navigationBar.alpha = 0 self.bottomView.alpha = 0 - self.micView.frame = CGRect(x: 18, y: ScreenHeight - BottomBarHeight - 14 - 12, width: 14, height: 14) } } } diff --git a/Sources/EaseCallUIKit/Classes/UI/Controllers/CallMultiViewController.swift b/Sources/EaseCallUIKit/Classes/UI/Controllers/CallMultiViewController.swift index ec2ecf0..e706057 100644 --- a/Sources/EaseCallUIKit/Classes/UI/Controllers/CallMultiViewController.swift +++ b/Sources/EaseCallUIKit/Classes/UI/Controllers/CallMultiViewController.swift @@ -62,7 +62,13 @@ open class CallMultiViewController: UIViewController { } public private(set) var role: CallRole = .caller - + + // 本地摄像头预览视图(未接听状态下使用) + public var localPreviewView: PixelBufferRenderView? + + // 跟踪摄像头状态 + public var isCameraPreviewEnabled: Bool = false + @objc public init(role: CallRole) { self.role = role super.init(nibName: nil, bundle: nil) @@ -88,7 +94,8 @@ open class CallMultiViewController: UIViewController { } else { self.bottomView.isCallConnected = false } - self.bottomView.updateButtonSelectedStatus(selectedIndex: 3) + // 初始化时不触发回调,只更新按钮状态 + self.bottomView.updateButtonSelectedStatus(selectedIndex: 3, triggerCallback: false) self.callView.isHidden = !state // Do any additional setup after loading the view. self.setupNavigationState() @@ -99,7 +106,6 @@ open class CallMultiViewController: UIViewController { self.bottomView.didTapButton = { [weak self] in self?.bottomClick(type: $0) } - CallKitManager.shared.enableLocalVideo(false) } func updateNavigationBar() { @@ -122,6 +128,11 @@ open class CallMultiViewController: UIViewController { func updateBottomState() { if self.connected { + // 连接成功后移除预览视图 + self.removeLocalPreview() + self.isCameraPreviewEnabled = false + + // 原有逻辑 self.callView.isHidden = !self.connected self.bottomView.animateToExpandedState() self.bottomView.isCallConnected = true @@ -168,7 +179,6 @@ open class CallMultiViewController: UIViewController { } @objc open func bottomClick(type: CallButtonType) { - switch type { case .mic_on: guard let currentUserId = ChatClient.shared().currentUsername,let item = CallKitManager.shared.itemsCache[currentUserId],let canvas = CallKitManager.shared.canvasCache[currentUserId] else { @@ -189,22 +199,39 @@ open class CallMultiViewController: UIViewController { case .flip_back: CallKitManager.shared.switchCamera() case .flip_front: CallKitManager.shared.switchCamera() case .camera_on: - guard let currentUserId = ChatClient.shared().currentUsername,let item = CallKitManager.shared.itemsCache[currentUserId],let canvas = CallKitManager.shared.canvasCache[currentUserId] else { - consoleLogInfo("CallMultiViewController: Current user not found in items cache.", type: .error) - return + if !self.connected { + // 未连接状态:显示全屏预览 + self.setupLocalPreview() + CallKitManager.shared.setupLocalVideo() + CallKitManager.shared.enableLocalVideo(true) + self.isCameraPreviewEnabled = true + } else { + // 已连接状态:正常处理(保持现有逻辑) + guard let currentUserId = ChatClient.shared().currentUsername,let item = CallKitManager.shared.itemsCache[currentUserId],let canvas = CallKitManager.shared.canvasCache[currentUserId] else { + consoleLogInfo("CallMultiViewController: Current user not found in items cache.", type: .error) + return + } + CallKitManager.shared.setupLocalVideo() + CallKitManager.shared.enableLocalVideo(true) + item.videoMuted = false + canvas.updateItem(item) } - CallKitManager.shared.setupLocalVideo() - CallKitManager.shared.enableLocalVideo(true) - item.videoMuted = false - canvas.updateItem(item) case .camera_off: - guard let currentUserId = ChatClient.shared().currentUsername,let item = CallKitManager.shared.itemsCache[currentUserId],let canvas = CallKitManager.shared.canvasCache[currentUserId] else { - consoleLogInfo("CallMultiViewController: Current user not found in items cache.", type: .error) - return + if !self.connected { + // 未连接状态:移除预览 + self.removeLocalPreview() + CallKitManager.shared.enableLocalVideo(false) + self.isCameraPreviewEnabled = false + } else { + // 已连接状态:正常处理(保持现有逻辑) + guard let currentUserId = ChatClient.shared().currentUsername,let item = CallKitManager.shared.itemsCache[currentUserId],let canvas = CallKitManager.shared.canvasCache[currentUserId] else { + consoleLogInfo("CallMultiViewController: Current user not found in items cache.", type: .error) + return + } + CallKitManager.shared.enableLocalVideo(false) + item.videoMuted = true + canvas.updateItem(item) } - CallKitManager.shared.enableLocalVideo(false) - item.videoMuted = true - canvas.updateItem(item) case .speaker_on: CallKitManager.shared.turnSpeakerOn(on: true) case .speaker_off: @@ -214,6 +241,13 @@ open class CallMultiViewController: UIViewController { self.dismiss(animated: true, completion: nil) CallKitManager.shared.hangup() case .accept: + // 保存摄像头开启状态(在移除预览前检查) + let wasCameraOn = self.isCameraPreviewEnabled + + // 接受通话前先移除预览视图 + self.removeLocalPreview() + self.isCameraPreviewEnabled = false + if let call = CallKitManager.shared.callInfo { GlobalTimerManager.shared.registerListener(self, timerIdentify: "call-\(call.channelName)-answering-timer") GlobalTimerManager.shared.registerListener(CallKitManager.shared, timerIdentify: "call-\(call.channelName)-answering-timer") @@ -228,6 +262,32 @@ open class CallMultiViewController: UIViewController { CallKitManager.shared.accept() } self.callView.isHidden = false + + // 如果接听前摄像头是开启的,需要同步状态到 MultiPersonCallView + if wasCameraOn { + consoleLogInfo("CallMultiViewController: Camera was on before accept, restoring state...", type: .debug) + + // accept() 方法会调用 enableLocalVideo(false),需要立即覆盖 + CallKitManager.shared.setupLocalVideo() + CallKitManager.shared.enableLocalVideo(true) + + // 延迟更新,确保 canvas 已创建并同步状态 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + guard let self = self else { return } + if let currentUserId = ChatClient.shared().currentUsername { + if let item = CallKitManager.shared.itemsCache[currentUserId], + let canvas = CallKitManager.shared.canvasCache[currentUserId] { + item.videoMuted = false + canvas.updateItem(item) + // 触发 MultiPersonCallView 更新 + self.callView.updateWithItems() + consoleLogInfo("CallMultiViewController: Camera state synced - userId=\(currentUserId), videoMuted=false", type: .debug) + } else { + consoleLogInfo("CallMultiViewController: Failed to sync camera state - item or canvas not found for userId=\(currentUserId)", type: .error) + } + } + } + } case .end: if let call = CallKitManager.shared.callInfo { GlobalTimerManager.shared.removeListener(self, timerIdentify: "call-\(call.channelName)-answering-timer") @@ -272,6 +332,9 @@ open class CallMultiViewController: UIViewController { } open override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + // 页面关闭时清理预览 + self.removeLocalPreview() + self.isCameraPreviewEnabled = false super.dismiss(animated: flag, completion: completion) } @@ -353,6 +416,28 @@ open class CallMultiViewController: UIViewController { } } } + + // 设置本地摄像头预览(全屏) + private func setupLocalPreview() { + guard localPreviewView == nil else { return } + + let previewView = PixelBufferRenderView(frame: self.view.bounds) + previewView.backgroundColor = .clear + previewView.userId = ChatClient.shared().currentUsername ?? "" + previewView.dragEnable = false + previewView.tag = 9999 // 特殊标记 + + // 插入到背景和 navigationBar 之间 + self.view.insertSubview(previewView, aboveSubview: self.background) + + self.localPreviewView = previewView + } + + // 移除本地摄像头预览 + private func removeLocalPreview() { + localPreviewView?.removeFromSuperview() + localPreviewView = nil + } } extension CallMultiViewController: TimerServiceListener { diff --git a/Sources/EaseCallUIKit/Classes/UI/Views/MultiCallBottomView.swift b/Sources/EaseCallUIKit/Classes/UI/Views/MultiCallBottomView.swift index 4635ce8..c9baa6f 100644 --- a/Sources/EaseCallUIKit/Classes/UI/Views/MultiCallBottomView.swift +++ b/Sources/EaseCallUIKit/Classes/UI/Views/MultiCallBottomView.swift @@ -51,7 +51,7 @@ public class MultiCallBottomView: UIView { } // 添加需要在通话接通后才能使用的按钮索引 - private let requiresConnectionButtonIndexes = [3] // Camera 按钮 + private let requiresConnectionButtonIndexes: [Int] = [] // 不再限制任何按钮 // 更新Flip按钮状态(基于Camera状态) private func updateFlipButtonState() { @@ -146,13 +146,13 @@ public class MultiCallBottomView: UIView { buttonView.tag = index 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 { // 提供视觉反馈但不执行操作 @@ -399,25 +399,25 @@ public class MultiCallBottomView: UIView { return positions } - func updateButtonSelectedStatus(selectedIndex: Int) { + func updateButtonSelectedStatus(selectedIndex: Int, triggerCallback: Bool = true) { guard selectedIndex >= 0 && selectedIndex < buttonViews.count else { consoleLogInfo("MultiCallBottomView: Invalid index for updating button status.", type: .error) return } - + // 更新按钮状态 let buttonView = buttonViews[selectedIndex] 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) { + + // 执行对应的操作(可选是否触发回调) + if triggerCallback, let actionType = getActionType(for: data, button: buttonView) { didTapButton?(actionType) } } From 016e6a2c0698eae16e7240919c249baeac5dd56f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E7=BB=A7=E8=B6=85?= <984065974@qq.com> Date: Thu, 6 Nov 2025 17:40:45 +0800 Subject: [PATCH 2/3] HIM19562 --- .../Classes/CoreService/Implements/CallKitManager+RTC.swift | 3 --- .../Classes/CoreService/Implements/CallKitManager.swift | 4 ++++ .../Classes/UI/Controllers/Call1v1VideoViewController.swift | 1 + .../EaseCallUIKit/Classes/UI/Views/MultiCallBottomView.swift | 3 +++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager+RTC.swift b/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager+RTC.swift index dd367fa..b66e1f9 100644 --- a/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager+RTC.swift +++ b/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager+RTC.swift @@ -56,9 +56,6 @@ extension CallKitManager: CallActionService { /// Set up local video capturing and rendering func setupLocalVideo() { - let cameraConfig = AgoraCameraCapturerConfiguration() - cameraConfig.cameraDirection = .front - self.engine?.setCameraCapturerConfiguration(cameraConfig) self.engine?.enableVideo() self.engine?.enableAudio() if let call = self.callInfo { diff --git a/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager.swift b/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager.swift index 4b71279..ea06e26 100644 --- a/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager.swift +++ b/Sources/EaseCallUIKit/Classes/CoreService/Implements/CallKitManager.swift @@ -158,6 +158,10 @@ public let CallKitVersion = "1.0.0" configuration.dimensions = CGSize(width: 1280, height: 720) configuration.frameRate = .fps30 self.engine?.setVideoEncoderConfiguration(configuration) + + let cameraConfig = AgoraCameraCapturerConfiguration() + cameraConfig.cameraDirection = .front + self.engine?.setCameraCapturerConfiguration(cameraConfig) for listener in self.listeners.allObjects { if let engine = self.engine { listener.onRtcEngineCreated?(engine: engine) diff --git a/Sources/EaseCallUIKit/Classes/UI/Controllers/Call1v1VideoViewController.swift b/Sources/EaseCallUIKit/Classes/UI/Controllers/Call1v1VideoViewController.swift index 3c315eb..74f0aa0 100644 --- a/Sources/EaseCallUIKit/Classes/UI/Controllers/Call1v1VideoViewController.swift +++ b/Sources/EaseCallUIKit/Classes/UI/Controllers/Call1v1VideoViewController.swift @@ -265,6 +265,7 @@ open class Call1v1VideoViewController: UIViewController { } @objc open func bottomClick(type: CallButtonType) { + print("bottomClick type:\(type.rawValue)") switch type { case .mic_on: CallKitManager.shared.enableLocalAudio(true) case .mic_off: CallKitManager.shared.enableLocalAudio(false) diff --git a/Sources/EaseCallUIKit/Classes/UI/Views/MultiCallBottomView.swift b/Sources/EaseCallUIKit/Classes/UI/Views/MultiCallBottomView.swift index c9baa6f..10633c2 100644 --- a/Sources/EaseCallUIKit/Classes/UI/Views/MultiCallBottomView.swift +++ b/Sources/EaseCallUIKit/Classes/UI/Views/MultiCallBottomView.swift @@ -132,6 +132,7 @@ public class MultiCallBottomView: UIView { private func setupButtonViews() { // 创建所有按钮视图 + buttonData.first?.isSelected = false for (index, data) in buttonData.enumerated() { let buttonView = CallButtonView(frame: CGRect(origin: .zero, size: CGSize(width: buttonWidth, height: buttonHeight)),iconTitleSpace: 4) if index == 4 { @@ -162,6 +163,7 @@ public class MultiCallBottomView: UIView { UIImpactFeedbackGenerator.impactOccurred(style: .medium) if let buttonData = button.data { + print("bottomClick title:\(buttonData.title) isSelected:\(buttonData.isSelected) status:\(buttonData.status) tag:\(button.tag)") buttonData.isSelected.toggle() buttonView.configure(data: buttonData) @@ -171,6 +173,7 @@ public class MultiCallBottomView: UIView { } if let buttonType = self.getActionType(for: buttonData, button: button) { + print("bottomClick type:\(buttonType) title:\(buttonData.title) isSelected:\(buttonData.isSelected) status:\(buttonData.status)") self.didTapButton?(buttonType) } } From 968398ad3acf9607f6aedb6aef1970581b6b3610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E7=BB=A7=E8=B6=85?= <984065974@qq.com> Date: Thu, 6 Nov 2025 17:41:16 +0800 Subject: [PATCH 3/3] Update EaseCallUIKit.podspec --- EaseCallUIKit.podspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EaseCallUIKit.podspec b/EaseCallUIKit.podspec index ab21342..cd1cc43 100644 --- a/EaseCallUIKit.podspec +++ b/EaseCallUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'EaseCallUIKit' - s.version = '4.17.0' + s.version = '4.18.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.17.0' + s.dependency 'HyphenateChat','>= 4.18.0' s.dependency 'AgoraRtcEngine_iOS/RtcBasic', '~> 4.6.0' end