import ReplayKit import UIKit import AVFoundation /// Broadcast Upload Extension 的核心处理类 /// 负责接收屏幕内容并叠加字幕悬浮窗 class SampleHandler: RPBroadcastSampleHandler { // MARK: - 属性 private var overlayWindow: UIWindow? private var subtitleLabel: UILabel? private var containerView: UIView? private let appGroupIdentifier = "group.com.saitong.voitrans.shared" // MARK: - 生命周期方法 override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) { print("🎬 ReplayKit 广播开始") setupOverlayWindow() startMonitoringSubtitleUpdates() } override func broadcastPaused() { print("⏸️ ReplayKit 广播暂停") hideOverlay() } override func broadcastResumed() { print("▶️ ReplayKit 广播恢复") showOverlay() } override func broadcastFinished() { print("🛑 ReplayKit 广播结束") cleanupOverlay() } // MARK: - 样本处理方法 override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) { switch sampleBufferType { case RPSampleBufferType.video: // 处理视频帧 - 这里我们主要用于触发悬浮窗显示 break case RPSampleBufferType.audioApp: // 处理应用音频 break case RPSampleBufferType.audioMic: // 处理麦克风音频 break @unknown default: break } } // MARK: - 悬浮窗设置 /// 设置悬浮窗口 private func setupOverlayWindow() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } // 创建悬浮窗口 self.overlayWindow = UIWindow(frame: UIScreen.main.bounds) self.overlayWindow?.windowLevel = UIWindow.Level.alert + 1000 self.overlayWindow?.backgroundColor = UIColor.clear self.overlayWindow?.isHidden = false // 创建容器视图 self.containerView = UIView() self.containerView?.backgroundColor = UIColor.black.withAlphaComponent(0.8) self.containerView?.layer.cornerRadius = 12 self.containerView?.layer.masksToBounds = true self.containerView?.translatesAutoresizingMaskIntoConstraints = false // 创建字幕标签 self.subtitleLabel = UILabel() self.subtitleLabel?.textColor = .white self.subtitleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium) self.subtitleLabel?.numberOfLines = 0 self.subtitleLabel?.textAlignment = .center self.subtitleLabel?.translatesAutoresizingMaskIntoConstraints = false self.subtitleLabel?.text = "等待翻译内容..." // 添加视图层次 guard let containerView = self.containerView, let subtitleLabel = self.subtitleLabel, let overlayWindow = self.overlayWindow else { return } containerView.addSubview(subtitleLabel) overlayWindow.addSubview(containerView) // 设置约束 NSLayoutConstraint.activate([ // 容器视图约束 - 位于屏幕顶部 containerView.topAnchor.constraint(equalTo: overlayWindow.safeAreaLayoutGuide.topAnchor, constant: 20), containerView.leadingAnchor.constraint(greaterThanOrEqualTo: overlayWindow.leadingAnchor, constant: 20), containerView.trailingAnchor.constraint(lessThanOrEqualTo: overlayWindow.trailingAnchor, constant: -20), containerView.centerXAnchor.constraint(equalTo: overlayWindow.centerXAnchor), containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 60), // 字幕标签约束 subtitleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 12), subtitleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), subtitleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16), subtitleLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -12) ]) print("✅ 悬浮窗口设置完成") } } /// 显示悬浮窗 private func showOverlay() { DispatchQueue.main.async { [weak self] in self?.overlayWindow?.isHidden = false self?.overlayWindow?.alpha = 1.0 } } /// 隐藏悬浮窗 private func hideOverlay() { DispatchQueue.main.async { [weak self] in UIView.animate(withDuration: 0.3) { self?.overlayWindow?.alpha = 0.0 } completion: { _ in self?.overlayWindow?.isHidden = true } } } /// 清理悬浮窗 private func cleanupOverlay() { DispatchQueue.main.async { [weak self] in self?.overlayWindow?.isHidden = true self?.overlayWindow = nil self?.subtitleLabel = nil self?.containerView = nil } } // MARK: - 字幕更新监听 /// 开始监听字幕更新 private func startMonitoringSubtitleUpdates() { // 使用 UserDefaults 监听字幕更新 if let userDefaults = UserDefaults(suiteName: appGroupIdentifier) { // 设置初始值监听 userDefaults.addObserver(self, forKeyPath: "subtitle_text", options: [.new], context: nil) userDefaults.addObserver(self, forKeyPath: "subtitle_source_language", options: [.new], context: nil) userDefaults.addObserver(self, forKeyPath: "subtitle_target_language", options: [.new], context: nil) print("✅ 开始监听字幕更新") } } /// KVO 监听回调 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == "subtitle_text" || keyPath == "subtitle_source_language" || keyPath == "subtitle_target_language" { updateSubtitleDisplay() } } /// 更新字幕显示 private func updateSubtitleDisplay() { guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else { return } let subtitleText = userDefaults.string(forKey: "subtitle_text") ?? "" let sourceLanguage = userDefaults.string(forKey: "subtitle_source_language") ?? "" let targetLanguage = userDefaults.string(forKey: "subtitle_target_language") ?? "" let isIntermediate = userDefaults.bool(forKey: "subtitle_is_intermediate") DispatchQueue.main.async { [weak self] in guard let self = self, let subtitleLabel = self.subtitleLabel else { return } if subtitleText.isEmpty { subtitleLabel.text = "等待翻译内容..." subtitleLabel.alpha = 0.6 } else { // 构建显示文本 var displayText = subtitleText if !sourceLanguage.isEmpty && !targetLanguage.isEmpty { displayText = "\(sourceLanguage) → \(targetLanguage)\n\(subtitleText)" } subtitleLabel.text = displayText subtitleLabel.alpha = isIntermediate ? 0.8 : 1.0 // 添加淡入动画 UIView.animate(withDuration: 0.2) { self.containerView?.transform = CGAffineTransform(scaleX: 1.05, y: 1.05) } completion: { _ in UIView.animate(withDuration: 0.2) { self.containerView?.transform = .identity } } } print("📝 字幕更新: \(subtitleText)") } } // MARK: - 清理 deinit { // 移除 KVO 监听 if let userDefaults = UserDefaults(suiteName: appGroupIdentifier) { userDefaults.removeObserver(self, forKeyPath: "subtitle_text") userDefaults.removeObserver(self, forKeyPath: "subtitle_source_language") userDefaults.removeObserver(self, forKeyPath: "subtitle_target_language") } cleanupOverlay() print("🗑️ SampleHandler 已清理") } }