You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

216 lines
8.5 KiB

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 已清理")
}
}