Browse Source

add

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
3bfefbdc6d
  1. 8
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  2. 4
      ios/Runner.xcodeproj/project.pbxproj
  3. 21
      ios/Runner/AppDelegate.swift
  4. 27
      ios/Runner/AudioSessionManager.swift
  5. 36
      ios/Runner/AzureAsrHelper.swift
  6. 2
      ios/Runner/ClassicBluetoothHelper.swift
  7. 291
      ios/Runner/DialogPipView.swift
  8. 179
      ios/Runner/VoiceInteractionService.swift
  9. 6
      lib/data/services/classic_bluetooth_service.dart

8
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,

4
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 = "<group>"; };
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 = "<group>"; };
0A18952A2D8F049C00675C94 /* DialogPipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialogPipView.swift; sourceTree = "<group>"; };
0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicBluetoothHelper.swift; sourceTree = "<group>"; };
0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMediaButtonHelper.swift; sourceTree = "<group>"; };
0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceInteractionService.swift; sourceTree = "<group>"; };
@ -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;

21
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)
}
}
}
// 蓝牙事件流处理器

27
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 // 强制重新配置,确保设置生效
)

36
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

2
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)
}

291
ios/Runner/DialogPipView.swift

@ -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..<frameCount {
while !writerInput.isReadyForMoreMediaData {}
let presentationTime = CMTime(value: CMTimeValue(i), timescale: 10)
adaptor.append(pixelBuffer, withPresentationTime: presentationTime)
}
writerInput.markAsFinished()
assetWriter.finishWriting {}
}
// 等待视频写入完成
while assetWriter.status == .writing {}
// 创建AVPlayerItem
let asset = AVAsset(url: assetWriter.outputURL)
return AVPlayerItem(asset: asset)
}
// 如果上面的方法失败,创建一个简单的默认项
let emptyItem = AVPlayerItem(url: URL(fileURLWithPath: ""))
return emptyItem
}
// 从UIImage创建像素缓冲区
private func createPixelBuffer(fromImage image: UIImage, size: CGSize) -> 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
}
}

179
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)
}
}

6
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<bool>('isBluetoothEnabled') ?? false;
bluetoothState.value = isEnabled ? 'on' : 'off';
Logger.info('初始蓝牙状态: ${bluetoothState.value}');
await _channel.invokeMethod('initialize');
// 设置事件监听
_setupBluetoothStateListener();

Loading…
Cancel
Save