From 3bfefbdc6d151b3dcdd1e4e0ebd1b0aca3e6d307 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Sat, 22 Mar 2025 20:28:30 +0000 Subject: [PATCH] add --- .../com/example/deep_voice/MainActivity.kt | 8 +- ios/Runner.xcodeproj/project.pbxproj | 4 - ios/Runner/AppDelegate.swift | 21 +- ios/Runner/AudioSessionManager.swift | 27 +- ios/Runner/AzureAsrHelper.swift | 36 ++- ios/Runner/ClassicBluetoothHelper.swift | 2 +- ios/Runner/DialogPipView.swift | 291 ------------------ ios/Runner/VoiceInteractionService.swift | 179 +---------- .../services/classic_bluetooth_service.dart | 6 + 9 files changed, 50 insertions(+), 524 deletions(-) delete mode 100644 ios/Runner/DialogPipView.swift diff --git a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt index 7ddd8cbe2..f6525fbf9 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt @@ -48,7 +48,7 @@ class MainActivity: FlutterActivity() { val timestamp = intent.getLongExtra("timestamp", 0) Log.d(TAG, "收到语音识别启动广播: timestamp=$timestamp") sendVoiceInteractionEvent(mapOf( - "type" to "recognitionStarted", + "type" to "recognition_started", "timestamp" to timestamp )) } @@ -640,6 +640,10 @@ class MainActivity: FlutterActivity() { result.error("BLUETOOTH_ERROR", "检查蓝牙状态失败: ${e.message}", e.toString()) } } + + "initialize" -> { + result.success(true) + } else -> result.notImplemented() } } @@ -724,7 +728,7 @@ class MainActivity: FlutterActivity() { runOnUiThread { try { val eventData = mapOf( - "type" to "chatHistory", + "type" to "chat_history_updated", "agentId" to agentId, "userMessage" to userMessage, "assistantMessage" to assistantMessage, diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index aab918c9b..afed04887 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 0A18952B2D8F049C00675C94 /* DialogPipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A18952A2D8F049C00675C94 /* DialogPipView.swift */; }; 0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */; }; 0A5EDC042D8B544100215E26 /* BluetoothMediaButtonHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */; }; 0A7A32E42D8DBA6D0047F746 /* VoiceInteractionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */; }; @@ -54,7 +53,6 @@ /* Begin PBXFileReference section */ 02CF74744224D1C933681603 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 0906EF7A73CBA5EAFC64E05D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 0A18952A2D8F049C00675C94 /* DialogPipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialogPipView.swift; sourceTree = ""; }; 0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicBluetoothHelper.swift; sourceTree = ""; }; 0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMediaButtonHelper.swift; sourceTree = ""; }; 0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceInteractionService.swift; sourceTree = ""; }; @@ -163,7 +161,6 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( - 0A18952A2D8F049C00675C94 /* DialogPipView.swift */, 0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */, 0A7A32E32D8DBA6D0047F746 /* VolcanoAIService.swift */, 0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */, @@ -434,7 +431,6 @@ 0A7A32E42D8DBA6D0047F746 /* VoiceInteractionService.swift in Sources */, 0A7A32E52D8DBA6D0047F746 /* VolcanoAIService.swift in Sources */, 0ADE29702D8C6C450032BFCB /* AudioSessionManager.swift in Sources */, - 0A18952B2D8F049C00675C94 /* DialogPipView.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index d711d9000..3ac160c31 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -30,9 +30,7 @@ import AVFoundation // 初始化音频会话管理器 audioSessionManager = AudioSessionManager.shared - // 配置音频会话支持画中画 - configureAudioSessionForPictureInPicture() - + // 统一注册所有方法通道 registerMethodChannel() @@ -107,10 +105,8 @@ import AVFoundation result(devices) } } - - case "startBluetoothDeviceMonitoring": - // 启动蓝牙设备监控 - classicBluetoothHelper.startBluetoothDeviceMonitoring() + case "initialize": + classicBluetoothHelper.initialize() result(true) default: @@ -528,16 +524,7 @@ import AVFoundation ) } - // 配置音频会话支持画中画 - private func configureAudioSessionForPictureInPicture() { - do { - let audioSession = AVAudioSession.sharedInstance() - try audioSession.setCategory(.playback, mode: .default, options: [.allowBluetooth, .allowBluetoothA2DP, .mixWithOthers]) - try audioSession.setActive(true) - } catch { - NSLog("AppDelegate: 配置音频会话失败: %@", error.localizedDescription) - } - } + } // 蓝牙事件流处理器 diff --git a/ios/Runner/AudioSessionManager.swift b/ios/Runner/AudioSessionManager.swift index 7c77c62a2..6020101d1 100644 --- a/ios/Runner/AudioSessionManager.swift +++ b/ios/Runner/AudioSessionManager.swift @@ -32,21 +32,7 @@ import UIKit super.init() NSLog("[AudioSessionManager] 音频会话管理器已初始化") setupNotifications() - - // do { - // try configureSession( - // category: .playAndRecord, - // mode: .spokenAudio, - // options: [ - // // .allowBluetooth, // 允许通过蓝牙SCO连接进行输入和输出 - // .allowBluetoothA2DP, // 允许通过蓝牙A2DP连接进行高质量音频输出 - // ], - // force: true // 强制重新配置,确保设置生效 - // ) - // NSLog("[AudioSessionManager] 初始化音频会话配置成功") - // } catch { - // NSLog("[AudioSessionManager] 初始化音频会话配置失败: \(error.localizedDescription)") - // } + } deinit { @@ -169,11 +155,6 @@ import UIKit @objc private func handleAppDidEnterBackground() { NSLog("[AudioSessionManager] 应用进入后台") - do { - try audioSession.setActive(true) - } catch { - print("后台激活 AVAudioSession 失败: \(error.localizedDescription)") - } // 应用进入后台时可能需要调整音频会话 } @@ -487,9 +468,9 @@ import UIKit options: [ // .allowBluetooth, // 允许通过蓝牙SCO连接进行输入和输出 .allowBluetoothA2DP, // 允许通过蓝牙A2DP连接进行高质量音频输出 - .defaultToSpeaker, // 默认使用扬声器输出 - .duckOthers, // 降低其他应用的音量 - .interruptSpokenAudioAndMixWithOthers // 中断其他语音音频并混合 + // .defaultToSpeaker, // 默认使用扬声器输出 + // .duckOthers, // 降低其他应用的音量 + // .interruptSpokenAudioAndMixWithOthers // 中断其他语音音频并混合 ], force: force // 强制重新配置,确保设置生效 ) diff --git a/ios/Runner/AzureAsrHelper.swift b/ios/Runner/AzureAsrHelper.swift index 9222b1321..390974877 100644 --- a/ios/Runner/AzureAsrHelper.swift +++ b/ios/Runner/AzureAsrHelper.swift @@ -337,20 +337,36 @@ class AzureAsrHelper: NSObject { // 先标记为非活跃状态,防止重复调用 _isContinuousRecognitionActive = false - do { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - do { - try recognizer.stopContinuousRecognition() - } catch { + // do { + // try recognizer.stopContinuousRecognition() + + // } catch { + // print("[AzureAsrHelper] 错误: 停止连续识别失败: \(error.localizedDescription)") + // } + + // // 异步执行停止操作,避免阻塞主线程 + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + guard let self = self else { return } + + do { + // 在后台线程执行停止操作 + try recognizer.stopContinuousRecognition() + + // 主线程回调通知结果 + DispatchQueue.main.async { + print("[AzureAsrHelper] 连续识别已停止") + self.eventHandler("sessionStopped", [:]) + } + } catch { + // 主线程回调通知错误 + DispatchQueue.main.async { print("[AzureAsrHelper] 错误: 停止连续识别失败: \(error.localizedDescription)") + // 停止失败,恢复状态 + self._isContinuousRecognitionActive = true + self.eventHandler("error", ["error": "停止识别失败: \(error.localizedDescription)"]) } } - - } catch { - print("[AzureAsrHelper] 错误: 停止连续识别失败: \(error.localizedDescription)") } - // 立即返回,不阻塞调用方 return true diff --git a/ios/Runner/ClassicBluetoothHelper.swift b/ios/Runner/ClassicBluetoothHelper.swift index 6bea951df..2011670c5 100644 --- a/ios/Runner/ClassicBluetoothHelper.swift +++ b/ios/Runner/ClassicBluetoothHelper.swift @@ -64,7 +64,7 @@ import UIKit } // 启动蓝牙设备监控 - @objc func startBluetoothDeviceMonitoring() { + @objc func initialize() { if centralManager == nil { centralManager = CBCentralManager(delegate: self, queue: nil) } diff --git a/ios/Runner/DialogPipView.swift b/ios/Runner/DialogPipView.swift deleted file mode 100644 index f4e17f81b..000000000 --- a/ios/Runner/DialogPipView.swift +++ /dev/null @@ -1,291 +0,0 @@ -import UIKit -import AVFoundation - -/** - * 带内边距的标签 - */ -class PaddingLabel: UILabel { - var padding: UIEdgeInsets - - init(padding: UIEdgeInsets) { - self.padding = padding - super.init(frame: .zero) - } - - required init?(coder: NSCoder) { - self.padding = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - super.init(coder: coder) - } - - override func drawText(in rect: CGRect) { - super.drawText(in: rect.inset(by: padding)) - } - - override var intrinsicContentSize: CGSize { - let size = super.intrinsicContentSize - return CGSize(width: size.width + padding.left + padding.right, - height: size.height + padding.top + padding.bottom) - } -} - -/** - * 对话画中画视图 - * - * 用于在画中画模式下显示用户和AI的对话内容 - */ -@available(iOS 13.0, *) -class DialogPipView: UIView { - - // 视频播放器图层(必须有,否则画中画无法工作) - private var playerLayer: AVPlayerLayer? - private var player: AVPlayer? - - // 对话显示视图 - private let dialogView = UIView() - - // 用户消息标签 - private let userMessageLabel: PaddingLabel = { - let label = PaddingLabel(padding: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)) - label.font = UIFont.systemFont(ofSize: 14) - label.textColor = UIColor.white - label.numberOfLines = 0 - label.backgroundColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 0.7) - label.layer.cornerRadius = 8 - label.layer.masksToBounds = true - label.textAlignment = .left - label.isHidden = true - return label - }() - - // AI助手消息标签 - private let assistantMessageLabel: PaddingLabel = { - let label = PaddingLabel(padding: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)) - label.font = UIFont.systemFont(ofSize: 14) - label.textColor = UIColor.white - label.numberOfLines = 0 - label.backgroundColor = UIColor(red: 0.0, green: 0.4, blue: 0.8, alpha: 0.7) - label.layer.cornerRadius = 8 - label.layer.masksToBounds = true - label.textAlignment = .left - label.isHidden = true - return label - }() - - // 初始化 - override init(frame: CGRect) { - super.init(frame: frame) - setupView() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setupView() - } - - // 设置视图 - private func setupView() { - backgroundColor = UIColor.black - - // 添加对话视图 - addSubview(dialogView) - dialogView.frame = bounds - dialogView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - - // 添加用户消息标签 - dialogView.addSubview(userMessageLabel) - userMessageLabel.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - userMessageLabel.leadingAnchor.constraint(equalTo: dialogView.leadingAnchor, constant: 8), - userMessageLabel.trailingAnchor.constraint(equalTo: dialogView.trailingAnchor, constant: -8), - userMessageLabel.topAnchor.constraint(equalTo: dialogView.topAnchor, constant: 10), - ]) - - // 添加AI助手消息标签 - dialogView.addSubview(assistantMessageLabel) - assistantMessageLabel.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - assistantMessageLabel.leadingAnchor.constraint(equalTo: dialogView.leadingAnchor, constant: 8), - assistantMessageLabel.trailingAnchor.constraint(equalTo: dialogView.trailingAnchor, constant: -8), - assistantMessageLabel.topAnchor.constraint(equalTo: userMessageLabel.bottomAnchor, constant: 10), - assistantMessageLabel.bottomAnchor.constraint(lessThanOrEqualTo: dialogView.bottomAnchor, constant: -10) - ]) - } - - // 创建并返回视频图层 - func setupDialogLayer() -> AVPlayerLayer { - // 创建无声视频,用于支持画中画功能 - let playerItem = createSilentVideoPlayerItem() - player = AVPlayer(playerItem: playerItem) - player?.actionAtItemEnd = .none - player?.play() - - // 创建视频图层 - let layer = AVPlayerLayer(player: player) - layer.frame = bounds - layer.videoGravity = .resizeAspectFill - - // 将视频图层放在最底层 - self.layer.insertSublayer(layer, at: 0) - playerLayer = layer - - // 注册通知,确保视频循环播放 - NotificationCenter.default.addObserver(self, - selector: #selector(playerItemDidReachEnd), - name: .AVPlayerItemDidPlayToEndTime, - object: player?.currentItem) - - return layer - } - - // 创建无声视频用于支持画中画 - private func createSilentVideoPlayerItem() -> AVPlayerItem { - // 创建一个简单的黑色视频,足够支持画中画但不会占用太多资源 - let composition = AVMutableComposition() - - // 添加视频轨道 - let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) - - // 创建一个简单的黑色视频,持续10秒 - let videoSize = CGSize(width: 640, height: 480) - let duration = CMTime(seconds: 10, preferredTimescale: 600) - - // 创建一个黑色图像 - UIGraphicsBeginImageContextWithOptions(videoSize, true, 0) - UIColor.black.setFill() - UIRectFill(CGRect(origin: .zero, size: videoSize)) - let blackImage = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - - // 创建黑色图像的视频 - let settings = [ - AVVideoCodecKey: AVVideoCodecType.h264, - AVVideoWidthKey: videoSize.width, - AVVideoHeightKey: videoSize.height - ] as [String : Any] - - let writerInput = AVAssetWriterInput(mediaType: .video, outputSettings: settings) - let adaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: writerInput, sourcePixelBufferAttributes: nil) - - if let pixelBuffer = createPixelBuffer(fromImage: blackImage!, size: videoSize) { - let attributes: [String: Any] = [ - kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB, - kCVPixelBufferWidthKey as String: videoSize.width, - kCVPixelBufferHeightKey as String: videoSize.height - ] - - // 创建一个5秒的黑色视频 - let assetWriter = try! AVAssetWriter(outputURL: URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("temp.mov"), fileType: .mov) - assetWriter.add(writerInput) - assetWriter.startWriting() - assetWriter.startSession(atSourceTime: .zero) - - let frameCount = 50 // 5秒,10fps - let frameDuration = CMTime(value: 1, timescale: 10) - - writerInput.requestMediaDataWhenReady(on: DispatchQueue.global()) { - for i in 0.. CVPixelBuffer? { - var pixelBuffer: CVPixelBuffer? - let attrs = [ - kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, - kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue - ] as CFDictionary - - let status = CVPixelBufferCreate( - kCFAllocatorDefault, - Int(size.width), - Int(size.height), - kCVPixelFormatType_32ARGB, - attrs, - &pixelBuffer - ) - - guard status == kCVReturnSuccess, let buffer = pixelBuffer else { - return nil - } - - CVPixelBufferLockBaseAddress(buffer, []) - let context = CGContext( - data: CVPixelBufferGetBaseAddress(buffer), - width: Int(size.width), - height: Int(size.height), - bitsPerComponent: 8, - bytesPerRow: CVPixelBufferGetBytesPerRow(buffer), - space: CGColorSpaceCreateDeviceRGB(), - bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue - ) - - context?.draw(image.cgImage!, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) - CVPixelBufferUnlockBaseAddress(buffer, []) - - return buffer - } - - // 视频播放结束处理 - @objc private func playerItemDidReachEnd(notification: Notification) { - // 循环播放视频 - player?.seek(to: .zero) - player?.play() - } - - // 更新对话内容 - func updateDialogContent(userMessage: String, assistantMessage: String) { - // 设置用户消息 - if !userMessage.isEmpty { - userMessageLabel.text = "用户: \(userMessage)" - userMessageLabel.isHidden = false - } else { - userMessageLabel.isHidden = true - } - - // 设置AI助手消息 - if !assistantMessage.isEmpty { - assistantMessageLabel.text = "AI助手: \(assistantMessage)" - assistantMessageLabel.isHidden = false - } else { - assistantMessageLabel.isHidden = true - } - - // 更新布局 - setNeedsLayout() - } - - // 布局子视图 - override func layoutSubviews() { - super.layoutSubviews() - - // 调整视频图层大小 - playerLayer?.frame = bounds - } - - // 释放资源 - deinit { - NotificationCenter.default.removeObserver(self) - player?.pause() - player = nil - } -} \ No newline at end of file diff --git a/ios/Runner/VoiceInteractionService.swift b/ios/Runner/VoiceInteractionService.swift index a8c25fc9e..e2ee54dac 100644 --- a/ios/Runner/VoiceInteractionService.swift +++ b/ios/Runner/VoiceInteractionService.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import AVFoundation -import AVKit /** * 语音交互服务,iOS原生实现 @@ -11,7 +10,6 @@ import AVKit * 2) 处理语音识别 * 3) 与火山AI服务交互 * 4) 文本转语音播放 - * 5) 画中画显示对话 */ @available(iOS 13.0, *) class VoiceInteractionService: NSObject { @@ -30,14 +28,6 @@ class VoiceInteractionService: NSObject { private var hasSpeechDetected = false private var isTtsSpeaking = false - // 画中画相关属性 - private var pipController: AVPictureInPictureController? - private var pipPossible = false - private var isPipActive = false - private var pipContentView: DialogPipView? - private var lastUserMessage = "" - private var lastAssistantMessage = "" - // 事件处理器 private weak var eventHandler: VoiceInteractionEventHandler? @@ -50,6 +40,8 @@ class VoiceInteractionService: NSObject { // 当前用户输入 private var currentUserInput = "" + private var lastUserMessage = "" + private var lastAssistantMessage = "" // 服务组件 private var azureAsrHelper: AzureAsrHelper? @@ -73,20 +65,7 @@ class VoiceInteractionService: NSObject { super.init() NSLog("%@: VoiceInteractionService 初始化中", TAG) - // 注册应用生命周期通知 - NotificationCenter.default.addObserver( - self, - selector: #selector(handleAppDidEnterBackground), - name: UIApplication.didEnterBackgroundNotification, - object: nil - ) - - NotificationCenter.default.addObserver( - self, - selector: #selector(handleAppWillEnterForeground), - name: UIApplication.willEnterForegroundNotification, - object: nil - ) + } /** @@ -111,12 +90,6 @@ class VoiceInteractionService: NSObject { // 初始化各个服务组件 initServices(azureKey: azureKey, azureRegion: azureRegion, volcanoKey: volcanoKey) - // 初始化画中画组件 - setupPictureInPicture() - - // 播放静音音频,确保音频会话活跃 - // startSilencePlayback() - // 启动monitorService定时器 silenceTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(monitorService), userInfo: nil, repeats: true) NSLog("%@: monitorService定时器已启动", TAG) @@ -151,9 +124,6 @@ class VoiceInteractionService: NSObject { // 停止TTS stopCurrentTTS() - // 停止画中画 - stopPictureInPicture() - // 释放服务实例 azureAsrHelper?.dispose() azureTtsHelper?.dispose() @@ -181,7 +151,6 @@ class VoiceInteractionService: NSObject { isTimeoutPaused = false hasSpeechDetected = false isTtsSpeaking = false - isPipActive = false lastUserMessage = "" lastAssistantMessage = "" } @@ -284,8 +253,6 @@ class VoiceInteractionService: NSObject { // 保存用户消息 lastUserMessage = text - // 更新画中画内容 - updatePictureInPictureContent() processWithVolcanoAI(text) } @@ -451,9 +418,6 @@ class VoiceInteractionService: NSObject { // 保存助手回复 self.lastAssistantMessage = response - // 更新画中画内容 - self.updatePictureInPictureContent() - // 播放AI回复 self.speakAIResponse(response) @@ -564,141 +528,4 @@ class VoiceInteractionService: NSObject { eventHandler?.sendEvent(eventData) } - /** - * 初始化画中画功能 - */ - private func setupPictureInPicture() { - NSLog("%@: 初始化画中画功能", TAG) - - // 创建用于画中画的视图 - let pipContentView = DialogPipView(frame: CGRect(x: 0, y: 0, width: 400, height: 300)) - self.pipContentView = pipContentView - - // 创建视频层 - let playerLayer = pipContentView.setupDialogLayer() - - // 检查画中画是否支持 - if AVPictureInPictureController.isPictureInPictureSupported() { - pipController = AVPictureInPictureController(playerLayer: playerLayer) - pipController?.delegate = self - pipPossible = true - NSLog("%@: 画中画控制器初始化完成", TAG) - - // 允许自动进入画中画模式 - if #available(iOS 14.2, *) { - pipController?.canStartPictureInPictureAutomaticallyFromInline = true - } - } else { - pipPossible = false - NSLog("%@: 当前设备不支持画中画功能", TAG) - } - } - - /** - * 更新画中画内容 - */ - private func updatePictureInPictureContent() { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - // 更新画中画视图内容 - self.pipContentView?.updateDialogContent( - userMessage: self.lastUserMessage, - assistantMessage: self.lastAssistantMessage - ) - } - } - - /** - * 开始画中画模式 - */ - func startPictureInPicture() { - NSLog("%@: 开始画中画pip", TAG) - guard pipPossible, let pipController = pipController, !isPipActive else { - NSLog("%@: 画中画功能无法启动或已处于活动状态", TAG) - return - } - - // 确保有对话内容才启动画中画 - if !lastUserMessage.isEmpty || !lastAssistantMessage.isEmpty { - pipController.startPictureInPicture() - } else { - NSLog("%@: 无对话内容,不启动画中画", TAG) - } - } - - /** - * 停止画中画模式 - */ - func stopPictureInPicture() { - guard pipPossible, let pipController = pipController, isPipActive else { - return - } - - pipController.stopPictureInPicture() - } - - /** - * 应用进入后台处理 - */ - @objc private func handleAppDidEnterBackground() { - NSLog("%@: 应用进入后台pip", TAG) - - // 如果有对话内容,自动开启画中画 - if !lastUserMessage.isEmpty || !lastAssistantMessage.isEmpty { - // 稍等片刻再启动画中画,确保转场动画完成 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - self?.startPictureInPicture() - } - } - } - - /** - * 应用即将进入前台处理 - */ - @objc private func handleAppWillEnterForeground() { - NSLog("%@: 应用即将进入前台pip", TAG) - - // 停止画中画 - if isPipActive { - stopPictureInPicture() - } - } -} - -// MARK: - AVPictureInPictureControllerDelegate -@available(iOS 13.0, *) -extension VoiceInteractionService: AVPictureInPictureControllerDelegate { - - func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { - NSLog("%@: 画中画即将启动", TAG) - } - - func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { - isPipActive = true - NSLog("%@: 画中画已启动", TAG) - sendEvent(type: "pip_started", data: [:]) - } - - func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) { - isPipActive = false - NSLog("%@: 画中画启动失败: %@", TAG, error.localizedDescription) - sendEvent(type: "pip_failed", data: ["error": error.localizedDescription]) - } - - func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { - NSLog("%@: 画中画即将停止", TAG) - } - - func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { - isPipActive = false - NSLog("%@: 画中画已停止", TAG) - sendEvent(type: "pip_stopped", data: [:]) - } - - func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { - NSLog("%@: 恢复画中画用户界面", TAG) - // 恢复界面完成后调用completionHandler - completionHandler(true) - } } \ No newline at end of file diff --git a/lib/data/services/classic_bluetooth_service.dart b/lib/data/services/classic_bluetooth_service.dart index d17c90211..89ec11ed4 100644 --- a/lib/data/services/classic_bluetooth_service.dart +++ b/lib/data/services/classic_bluetooth_service.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:permission_handler/permission_handler.dart'; import '../../core/utils/logger.dart'; +import 'dart:io'; /// 蓝牙设备信息 class BluetoothDevice { @@ -56,11 +57,16 @@ class ClassicBluetoothService extends GetxService { try { Logger.info('正在初始化蓝牙服务...'); + + // 获取当前蓝牙状态 final bool isEnabled = await _channel.invokeMethod('isBluetoothEnabled') ?? false; bluetoothState.value = isEnabled ? 'on' : 'off'; Logger.info('初始蓝牙状态: ${bluetoothState.value}'); + await _channel.invokeMethod('initialize'); + + // 设置事件监听 _setupBluetoothStateListener();