From 76a0649890d98c5e8cb77255f760b8c2e4ee5659 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Tue, 3 Feb 2026 17:32:45 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(ios):=20=E4=BF=AE=E5=A4=8DPiP=E9=9F=B3?= =?UTF-8?q?=E9=A2=91=E4=BC=9A=E8=AF=9D=E9=85=8D=E7=BD=AE=E4=BB=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=90=8E=E5=8F=B0=E6=B7=B7=E9=9F=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在iOS 14.5以下版本中,PiP功能缺少.mixWithOthers选项,可能导致音频中断。现在统一为所有支持的iOS版本添加此选项,确保PiP音频在后台能与其他音频混合播放。同时将音频会话初始化时机从插件注册时延迟到实际创建PiP视图时,避免不必要的音频会话激活。 --- .../ios/Classes/NativePlugin.swift | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/local_plugins/floating_ui_plugin/ios/Classes/NativePlugin.swift b/local_plugins/floating_ui_plugin/ios/Classes/NativePlugin.swift index 9dc4a1fad..ac1d71522 100644 --- a/local_plugins/floating_ui_plugin/ios/Classes/NativePlugin.swift +++ b/local_plugins/floating_ui_plugin/ios/Classes/NativePlugin.swift @@ -6,11 +6,19 @@ import UIKit public class FloatingUiPlugin: NSObject, FlutterPlugin { private var pipContentViewArray: [PlayerView] = [] + /** + * 更新音频会话配置,保证 PiP 播放场景的音频行为符合预期 + * - Parameters: 无 + * - Returns: 无 + * - Throws: 无(内部捕获并记录 setCategory/setActive 可能抛出的异常) + */ private static func updateAudioSession() { let audioSession = AVAudioSession.sharedInstance() do { if #available(iOS 14.5, *) { - try audioSession.setCategory(.playback, mode: .moviePlayback, options: [.overrideMutedMicrophoneInterruption]) + try audioSession.setCategory(.playback, mode: .moviePlayback, options: [.overrideMutedMicrophoneInterruption, .mixWithOthers]) + } else { + try audioSession.setCategory(.playback, mode: .moviePlayback, options: [.mixWithOthers]) } try audioSession.setActive(true) } catch { @@ -18,10 +26,14 @@ public class FloatingUiPlugin: NSObject, FlutterPlugin { } } + /** + * Flutter 插件注册入口 + * - Parameters: + * - registrar: FlutterPluginRegistrar 实例,用于注册通道与原生视图 + * - Returns: 无 + * - Throws: 无(注册流程不抛出异常) + */ public static func register(with registrar: FlutterPluginRegistrar) { - // 保证 PiP 能正常工作 - Self.updateAudioSession() - let channel = FlutterMethodChannel(name: "native_plugin", binaryMessenger: registrar.messenger()) let instance = FloatingUiPlugin() registrar.addMethodCallDelegate(instance, channel: channel) @@ -30,12 +42,21 @@ public class FloatingUiPlugin: NSObject, FlutterPlugin { registrar.register(factory, withId: "native_view") } + /** + * Flutter MethodChannel 调用处理入口 + * - Parameters: + * - call: Flutter 发起的方法调用(方法名与参数) + * - result: 用于向 Flutter 回传结果的回调 + * - Returns: 无 + * - Throws: 无(内部逻辑不抛出异常,异常通过 result 或日志处理) + */ public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case "getPlatformVersion": result("iOS " + UIDevice.current.systemVersion) case "createPipContentView": + Self.updateAudioSession() let playerView = PlayerView() // 这里未来可以根据 PiP 的事件来控制播放状态 playerView.play() From 5603d2a67e4d464b1f18572d1b3fb6548d64df05 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Wed, 4 Feb 2026 09:51:50 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(ios):=20=E4=BF=AE=E5=A4=8D=E9=9F=B3?= =?UTF-8?q?=E9=A2=91=E4=BC=9A=E8=AF=9D=E9=85=8D=E7=BD=AE=E4=BB=A5=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE=E5=A4=84=E7=90=86=E9=9F=B3=E4=B9=90=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E5=92=8CTTS=E7=9A=84=E9=9F=B3=E9=A2=91=E6=89=93=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修改音乐服务插件,移除音频会话的 mixWithOthers 选项,使应用播放音乐时能打断外部音乐 在播放、恢复、切换歌曲时重新设置音频会话,确保正确激活 在停止播放时停用音频会话并通知其他应用恢复播放 修改语音插件,在TTS播放时使用 mixWithOthers 替代 duckOthers 选项 添加空闲时停用音频会话的逻辑,避免影响其他应用的音频播放 同时更新应用版本号至 1.0.22 --- .../Sources/azure_speech/AzureTtsHelper.swift | 19 ++++++++++++++----- .../Sources/music_service/MusicService.swift | 9 +++++++-- pubspec.yaml | 2 +- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift index ceb195a59..e9b5bbc25 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift @@ -1413,6 +1413,7 @@ private let audioSessionQueueKey = DispatchSpecificKey() private var audioSessionQueueKeySet = false private var audioInterrupted = false private var audioSessionObserversAdded = false +private var shouldDeactivateAudioSessionWhenIdle = false // 新增:路由变化的去抖与配置重入保护标记 private var routeChangeDebounceWorkItem: DispatchWorkItem? private var isApplyingAudioSessionConfig = false @@ -1471,6 +1472,7 @@ private func applyAudioSessionForTTS() -> Bool { let otherAudioPlaying = audioSession.isOtherAudioPlaying let isRecording = (self.micCapture?.isCapturing == true) self.isHeadphones = hasHeadphones + self.shouldDeactivateAudioSessionWhenIdle = otherAudioPlaying let desiredCategory: AVAudioSession.Category let desiredMode: AVAudioSession.Mode @@ -1490,7 +1492,7 @@ private func applyAudioSessionForTTS() -> Bool { desiredOptions = [.allowBluetooth, .defaultToSpeaker] } if otherAudioPlaying { - desiredOptions.insert(.duckOthers) + desiredOptions.insert(.mixWithOthers) } } else if self.currentRecognitionMode == "push_to_talk" || self.currentRecognitionMode == "normal" || self.currentRecognitionMode == "ble_wakeup" { if isRecording { @@ -1500,7 +1502,7 @@ private func applyAudioSessionForTTS() -> Bool { desiredCategory = .playback desiredMode = .spokenAudio if otherAudioPlaying { - desiredOptions.insert(.duckOthers) + desiredOptions.insert(.mixWithOthers) } } else if isRecording { desiredCategory = .playAndRecord @@ -1515,13 +1517,13 @@ private func applyAudioSessionForTTS() -> Bool { desiredOptions.insert(.defaultToSpeaker) } if otherAudioPlaying { - desiredOptions.insert(.duckOthers) + desiredOptions.insert(.mixWithOthers) } } else { desiredCategory = .playback desiredMode = .spokenAudio if otherAudioPlaying { - desiredOptions.insert(.duckOthers) + desiredOptions.insert(.mixWithOthers) } } @@ -1551,7 +1553,7 @@ private func applyAudioSessionForTTS() -> Bool { try audioSession.setActive(true) } catch { do { - try audioSession.setCategory(.playback, mode: .default, options: []) + try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers]) try audioSession.setActive(true) } catch { success = false @@ -1645,6 +1647,13 @@ private func deactivateAudioSessionAfterTTSIfIdle() { self.audioEngine = nil self.playerNode = nil self.pcmFormat = nil + if self.shouldDeactivateAudioSessionWhenIdle { + do { + try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) + } catch { + } + self.shouldDeactivateAudioSessionWhenIdle = false + } } } if DispatchQueue.getSpecific(key: audioSessionQueueKey) != nil { diff --git a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift index 0eb558514..892d3b667 100644 --- a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift +++ b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift @@ -57,11 +57,11 @@ public class MusicService: NSObject { // MARK: - 音频会话设置 private func setupAudioSession() { do { - // 设置音频会话类别,添加 mixWithOthers 选项以确保通知正常显示 + // 设置音频会话类别:不使用 mixWithOthers,让本应用音乐播放时打断外部音乐 try AVAudioSession.sharedInstance().setCategory( .playback, mode: .default, - options: [.allowAirPlay, .allowBluetooth, .mixWithOthers] + options: [.allowAirPlay, .allowBluetooth] ) try AVAudioSession.sharedInstance().setActive(true) print("🎵 [MusicService] ✅ 音频会话设置成功") @@ -407,6 +407,8 @@ public class MusicService: NSObject { print("开始播放: \(url), ID: \(currentIdInternal ?? 0)") + setupAudioSession() + // 检查是否已在播放列表中 let existingIndex = playlist.firstIndex { existingSong in if let existingId = existingSong["id"] as? NSNumber, @@ -450,6 +452,7 @@ public class MusicService: NSObject { } public func resume() { + setupAudioSession() player?.play() isPlaying = true updatePlaybackState() @@ -495,6 +498,7 @@ public class MusicService: NSObject { updatePlaybackState() updateMediaSessionPlaybackState(isPlaying: false) stopProgressTimer() + _ = try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) print("已停止") } @@ -969,6 +973,7 @@ public class MusicService: NSObject { } // 开始播放 + setupAudioSession() player?.play() isPlaying = true updateMediaSessionPlaybackState(isPlaying: true) diff --git a/pubspec.yaml b/pubspec.yaml index 36afbc46b..b4bddf156 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: voitrans description: "Voitrans - AI Voice Assistant." publish_to: "none" -version: 1.0.21+72 +version: 1.0.22+72 environment: sdk: ">=3.3.0 <4.0.0"