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