diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index d8c160df3..ce41d888d 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -223,20 +223,28 @@ class BleManager extends GetxService { bool isFinal = false, }) async { // 段尾帧(pcm 为空 + isFinal=true)必须放行,让 native 触发立即整段下发; - // 仅对"普通空帧(非段尾)"短路。 + // 仅对“普通空帧(非段尾)”短路。 if (pcm.isEmpty && !isFinal) return true; final upper = leg.toUpperCase(); final streamId = (upper == 'A' || upper == 'UPLINK') ? 'out.uplink' : 'out.downlink'; + final isFlush = isFinal; try { - return await Jielihome.instance.feedTranslatedAudio( + final ok = await Jielihome.instance.feedTranslatedAudio( streamId: streamId, pcm: pcm, sampleRate: sampleRate, channels: channels, isFinal: isFinal, ); + if (isFlush || !ok) { + // 只在段尾 flush 或 native 返回 false 时打,避免刷屏。 + Logger.info( + '[BleManager] feedAstTtsPcm leg=$leg streamId=$streamId pcm=${pcm.length}B isFinal=$isFinal nativeOk=$ok', + ); + } + return ok; } catch (e) { Logger.w('BleManager', 'feedAstTtsPcm leg=$leg failed: $e'); return false; @@ -265,6 +273,12 @@ class BleManager extends GetxService { _eventSub?.cancel(); _eventSub = Jielihome.instance.events.listen( (event) { + // TranslationAudioEvent 频率极高(10ms/帧),最先拦截并单独走节流日志处理器, + // 避免进入下面的 print / if‑链路经引发刷屏。 + if (event is TranslationAudioEvent) { + _handleTranslationAudio(event); + return; + } // 总开关日志:每一个事件都打一次,便于诊断 native → dart 通道是否畅通 print('[BLE] [EVT] ${event.runtimeType}'); if (event is AdapterStatusEvent) { @@ -309,8 +323,6 @@ class BleManager extends GetxService { _isCall.value = event.status != 0; } else if (event is ExpandFunctionEvent) { _handleExpandFunction(event); - } else if (event is TranslationAudioEvent) { - _handleTranslationAudio(event); } else if (event is TranslationLogEvent) { print('[BLE] [EVT] TranslationLog modeId=${event.modeId} ${event.content}'); } else if (event is TranslationErrorEvent) { diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 254d282dc..550647be4 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -1318,8 +1318,16 @@ class TranslationController extends GetxController with WidgetsBindingObserver { }, onError: (e) => Logger.error('[CALL-BRIDGE] in.downlink stream error: $e'), ); + // 诊断:计数 + isFinal 必打,用于定位 “TTS 到底有没有被转发到 native”。 + int forwardCnt = 0; _jieliCallAstTtsSub = _astService.astTtsFrames.listen( (frame) { + forwardCnt++; + if (frame.isFinal || forwardCnt == 1 || forwardCnt % 50 == 0) { + Logger.info( + '[CALL-BRIDGE] forward TTS leg=${frame.leg} pcm=${frame.pcm.length}B isFinal=${frame.isFinal} (#$forwardCnt)', + ); + } bleManager.feedAstTtsPcm( leg: frame.leg, pcm: frame.pcm, diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift index b8aa70749..ae619c7da 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift @@ -59,6 +59,9 @@ import ble_service case tts case audioData case ast + // 通话翻译下行:A/B 两路 TTS PCM 以 {leg, pcm, final} 抩回 Dart, + // 由 translation_controller 转发给 device_jieli 回灌耳机。 + case astTts } /** * 通道配置结构体 @@ -93,6 +96,11 @@ import ble_service private var astChannel: FlutterMethodChannel? private var astEventChannel: FlutterEventChannel? internal var astEventSink: FlutterEventSink? + // 通话翻译 TTS 回灬事件通道(与 Android 的 ast_tts_events 对齐) + private var astTtsEventChannel: FlutterEventChannel? + internal var astTtsEventSink: FlutterEventSink? + // 通话翻译 TTS 发送计数,节流日志 + private var astTtsEmitCount: Int64 = 0 // 双向 AST 服务:A(己方)和 B(对方),与 Android 对齐 private let azureAstHelperA = IntegratedSpeechTranslationService() private let azureAstHelperB = IntegratedSpeechTranslationService() @@ -153,8 +161,10 @@ import ble_service fileprivate func pushAstAudioToA(_ data: Data) { astAudioPushCountA &+= 1 if astAudioPushCountA % 100 == 1 { - os_log("pushAstAudioToA: provider=%{public}@ size=%d count=%lld", - log: ctLog, type: .info, currentAstProvider, data.count, astAudioPushCountA) + let rms = Self.rmsOfPCM16LE(data) + let head = Self.hexPreview(data, 16) + os_log("pushAstAudioToA: provider=%{public}@ size=%d count=%lld rms=%.1f head=%{public}@", + log: ctLog, type: .info, currentAstProvider, data.count, astAudioPushCountA, rms, head) } let payload = gateAgainstBPlayback(data) switch currentAstProvider { @@ -171,8 +181,10 @@ import ble_service fileprivate func pushAstAudioToB(_ data: Data) { astAudioPushCountB &+= 1 if astAudioPushCountB % 100 == 1 { - os_log("pushAstAudioToB: provider=%{public}@ size=%d count=%lld", - log: ctLog, type: .info, currentAstProvider, data.count, astAudioPushCountB) + let rms = Self.rmsOfPCM16LE(data) + let head = Self.hexPreview(data, 16) + os_log("pushAstAudioToB: provider=%{public}@ size=%d count=%lld rms=%.1f head=%{public}@", + log: ctLog, type: .info, currentAstProvider, data.count, astAudioPushCountB, rms, head) } let payload = gateAgainstBPlayback(data) switch currentAstProvider { @@ -185,6 +197,28 @@ import ble_service } } + /// 计算 PCM16LE 的 RMS 能量(静音检测用)。返回 0 表示全 0 样本。 + private static func rmsOfPCM16LE(_ data: Data) -> Double { + guard data.count >= 2 else { return 0 } + let count = data.count / 2 + var sumSq: Double = 0 + data.withUnsafeBytes { (raw: UnsafeRawBufferPointer) in + let p = raw.bindMemory(to: Int16.self) + for i in 0.. String { + let take = min(n, data.count) + if take == 0 { return "" } + return data.prefix(take).map { String(format: "%02x", $0) }.joined() + } + // 音频数据相关 private var audioDataEventChannel: FlutterEventChannel? internal var audioDataEventSink: FlutterEventSink? @@ -240,6 +274,9 @@ import ble_service case .ast: self.astChannel = methodChannel self.astEventChannel = eventChannel + case .astTts: + // 不走 setupChannel 路径(在 register 里单独注册),这里仅为了详尽性。 + break } } /** @@ -264,6 +301,10 @@ private func sendEvent(_ event: [String: Any], type: EventType) { case .ast: eventSink = astEventSink logMessage = "无法发送AST事件:事件通道未准备好" + case .astTts: + // .astTts 不走 sendEvent 通用路径(单独走 pushAstTtsFrame),这里仅为了详尽性。 + eventSink = astTtsEventSink + logMessage = "无法发送AST-TTS事件:事件通道未准备好" } guard eventSink != nil else { @@ -313,6 +354,11 @@ private func sendEvent(_ event: [String: Any], type: EventType) { audioDataEventChannel.setStreamHandler(BaseEventStreamHandler(instance: instance, eventType: .audioData)) instance.audioDataEventChannel = audioDataEventChannel + // 通话翻译 TTS 回灬事件通道(只有事件通道,与 Android 对齐) + let astTtsEventChannel = FlutterEventChannel(name: "azure_speech/ast_tts_events", binaryMessenger: registrar.messenger()) + astTtsEventChannel.setStreamHandler(BaseEventStreamHandler(instance: instance, eventType: .astTts)) + instance.astTtsEventChannel = astTtsEventChannel + // 设置讯飞原生日志回调,通过 EventChannel 回传 Dart instance.azureAsrHelper.nativeLogCallback = { [weak instance] level, message in instance?.sendAsrEvent(["type": "nativeLog", "level": level, "message": message]) @@ -1148,6 +1194,36 @@ private func sendAudioDataEvent(_ event: [String: Any]) { } } + /// 通话翻译下行TTS PCM抩回 Dart,由 translation_controller 转发给 device_jieli 回灬耳机。 + /// - Parameters: + /// - leg: "A" → 对端听(SCO_UP_LINK);"B" → 本机听(SCO_DOWN_LINK) + /// - pcm: 16bit、单声道、16kHz PCM;段尾帧允许传空 + /// - isFinal: 是否是该段 utterance 的段尾(与 Android 合约一致,native 依靠 isFinal=true 触发整段下发) + internal func pushAstTtsFrame(leg: String, pcm: Data, isFinal: Bool) { + guard let sink = astTtsEventSink else { + // 调试:报警一次,避免刷屏 + astTtsEmitCount &+= 1 + if astTtsEmitCount <= 5 || astTtsEmitCount % 200 == 0 { + NSLog("[AST-TTS] DROPPED leg=%@ pcm=%dB isFinal=%@ (no sink yet, emitCount=%lld)", + leg, pcm.count, isFinal ? "true" : "false", astTtsEmitCount) + } + return + } + astTtsEmitCount &+= 1 + if isFinal || astTtsEmitCount <= 3 || astTtsEmitCount % 100 == 0 { + NSLog("[AST-TTS] emit leg=%@ pcm=%dB isFinal=%@ (#%lld)", + leg, pcm.count, isFinal ? "true" : "false", astTtsEmitCount) + } + let payload: [String: Any] = [ + "leg": leg, + "pcm": FlutterStandardTypedData(bytes: pcm), + "final": isFinal, + ] + DispatchQueue.main.async { + sink(payload) + } + } + /// 发送AST事件到Flutter层 internal func sendAstEvent(_ event: [String: Any]) { let eventType = event["type"] as? String ?? "unknown" @@ -1509,6 +1585,9 @@ private class BaseEventStreamHandler: NSObject, FlutterStreamHandler { print("音频数据事件通道已准备好") case .ast: instance?.astEventSink = events + case .astTts: + instance?.astTtsEventSink = events + print("[AST-TTS] event channel ready (azure_speech/ast_tts_events)") } return nil } @@ -1524,6 +1603,9 @@ private class BaseEventStreamHandler: NSObject, FlutterStreamHandler { print("音频数据事件通道已取消") case .ast: instance?.astEventSink = nil + case .astTts: + instance?.astTtsEventSink = nil + print("[AST-TTS] event channel cancelled") } return nil } @@ -1610,11 +1692,14 @@ private class DoubaoCallbackProxy: DoubaoE2ETranslateHelper.Callback { func onPartialAudio(sessionId: String, data: Data) { if data.isEmpty { return } + // 新路径:同时抩到 ast_tts_events sink,让通话翻译返回耳机走 device_jieli。 + // isFinal=false,native 会累积,等 onSessionFinished 返回后一次性下发。 + plugin?.pushAstTtsFrame(leg: serviceId, pcm: data, isFinal: false) if serviceId == "A" { - // A 路:翻译后的音频通过 BLE 写到耳机外放给对方 + // A 路:翻译后的音频通过 BLE 写到耳机外放给对方(遗留,已 deprecated noop) BleService.shared.writeExternalAudioData(data: data) } else { - // B 路:对方的翻译音频,本地 PCM 播放器直接播 + // B 路:对方的翻译音频,本地 PCM 播放器直接播(遗留) plugin?.pushCallBPcm(data) } } @@ -1624,6 +1709,9 @@ private class DoubaoCallbackProxy: DoubaoE2ETranslateHelper.Callback { let original = plugin?.doubaoFinalSourceTextCache.removeValue(forKey: key) ?? "" os_log("[DoubaoCallback-%{public}@] onSessionFinished finalText=%{public}@ original=%{public}@ audioLen=%d", log: ctLog, type: .info, serviceId, finalText, original, finalAudio.count) + // 段尾合约(与 Android markEnd 对齐):**不**重复写 finalAudio,只 emit 空 PCM + isFinal=true。 + // partial 阶段已累积到 native,这里只是告诉 device_jieli “这段结束了,可以整段下发”。 + plugin?.pushAstTtsFrame(leg: serviceId, pcm: Data(), isFinal: true) plugin?.sendAstEvent([ "type": "translated", "serviceId": serviceId, @@ -1638,6 +1726,8 @@ private class DoubaoCallbackProxy: DoubaoE2ETranslateHelper.Callback { func onSessionError(sessionId: String, code: Int, message: String) { os_log("[DoubaoCallback-%{public}@] onSessionError code=%d msg=%{public}@", log: ctLog, type: .error, serviceId, code, message) + // 错误也要 emit isFinal=true,避免 native 累积 buffer 残留(与 Android markEnd 对齐)。 + plugin?.pushAstTtsFrame(leg: serviceId, pcm: Data(), isFinal: true) plugin?.sendAstEvent([ "type": "error", "serviceId": serviceId, @@ -1648,6 +1738,15 @@ private class DoubaoCallbackProxy: DoubaoE2ETranslateHelper.Callback { ]) } + /// 句级 TTS 结束。豆包会在每说完一句后发 ttssentenceEnd(351),不是会话级 sessionFinished(152)。 + /// 这里 emit 空 PCM + isFinal=true,使 device_jieli 端把累积的整段 PCM 立即编码下发给 SDK。 + /// 不走 onSessionFinished 的理由:那一条会触发 WS cancel + 会话结束事件,与句级语义不符。 + func onSentenceEnd(sessionId: String) { + os_log("[DoubaoCallback-%{public}@] onSentenceEnd sessionId=%{public}@", + log: ctLog, type: .info, serviceId, sessionId) + plugin?.pushAstTtsFrame(leg: serviceId, pcm: Data(), isFinal: true) + } + func onFinalTranslatedText(sessionId: String, finalText: String) { let key = "\(serviceId):\(sessionId)" let original = plugin?.doubaoFinalSourceTextCache.removeValue(forKey: key) ?? "" @@ -1743,6 +1842,8 @@ private class AliyunCallbackProxy: AliyunBailianE2EHelper.Callback { func onPartialAudio(sessionId: String, data: Data) { if data.isEmpty { return } + // 新路径:同时抩到 ast_tts_events sink,走 device_jieli 回灬耳机。 + plugin?.pushAstTtsFrame(leg: serviceId, pcm: data, isFinal: false) if serviceId == "A" { BleService.shared.writeExternalAudioData(data: data) } else { @@ -1755,6 +1856,8 @@ private class AliyunCallbackProxy: AliyunBailianE2EHelper.Callback { let original = sourceTextCache.removeValue(forKey: key) ?? "" os_log("[AliyunCallback-%{public}@] onSessionFinished finalText=%{public}@ original=%{public}@ audioLen=%d", log: ctLog, type: .info, serviceId, finalText, original, finalAudio.count) + // 段尾合约(与 Android markEnd 对齐):**不**重复写 finalAudio,只 emit 空 PCM + isFinal=true。 + plugin?.pushAstTtsFrame(leg: serviceId, pcm: Data(), isFinal: true) plugin?.sendAstEvent([ "type": "translated", "serviceId": serviceId, @@ -1769,6 +1872,8 @@ private class AliyunCallbackProxy: AliyunBailianE2EHelper.Callback { func onSessionError(sessionId: String, code: Int, message: String) { os_log("[AliyunCallback-%{public}@] onSessionError code=%d msg=%{public}@", log: ctLog, type: .error, serviceId, code, message) + // 错误也要 emit isFinal=true,避免 native 累积 buffer 残留(与 Android markEnd 对齐)。 + plugin?.pushAstTtsFrame(leg: serviceId, pcm: Data(), isFinal: true) plugin?.sendAstEvent([ "type": "error", "serviceId": serviceId, diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/DoubaoE2ETranslateHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/DoubaoE2ETranslateHelper.swift index f37318904..a81291435 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/DoubaoE2ETranslateHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/DoubaoE2ETranslateHelper.swift @@ -22,6 +22,10 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate { func onPartialAudio(sessionId: String, data: Data) func onSessionFinished(sessionId: String, finalText: String, finalAudio: Data) func onSessionError(sessionId: String, code: Int, message: String) + /// 句级结束:豆包发的是 `ttssentenceEnd (351)`,不是会话级 `sessionFinished (152)`。 + /// 用来告诉调用方 “这一句 TTS PCM 发完了,应立即把 native 端累积的段所有 PCM 制作为一整段下发”。 + /// 与 Android `markEnd` 合约对齐。 + func onSentenceEnd(sessionId: String) } /** @@ -568,6 +572,21 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate { return } + // 句级 TTS 结束:豆包每说完一句发一次 ttssentenceEnd(351), + // 此时应立即触发 isFinal=true,让 device_jieli 把累积的 PCM 一次性下发给 SDK。 + // 与会话级 sessionFinished(152) 的关键区别:不关闭 WS,不清空 recvText/recvAudio、 + // 后续的 startNewSubSession 由 translationSubtitleEnd 控制。 + if event == .ttssentenceEnd { + os_log("TTSSentenceEnd: emit sentence isFinal", log: log, type: .info) + // 先 flush audioChunkBuffer 里不足 1280 倍数的尾料,避免最后几十字节永远满不下发。 + if !audioChunkBuffer.isEmpty { + callback?.onPartialAudio(sessionId: sessionId, data: audioChunkBuffer) + audioChunkBuffer = Data() + } + callback?.onSentenceEnd(sessionId: sessionId) + return + } + if !audioData.isEmpty { recvAudio.append(audioData) // 极高频(音频帧),取消 info 日志 @@ -639,6 +658,15 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate { webSocket?.cancel(with: .normalClosure, reason: "Completed".data(using: .utf8)) return } + // JSON 兜底:句级结束也走 onSentenceEnd。 + if event == "TTSSentenceEnd" { + if !audioChunkBuffer.isEmpty { + callback?.onPartialAudio(sessionId: sessionId, data: audioChunkBuffer) + audioChunkBuffer = Data() + } + callback?.onSentenceEnd(sessionId: sessionId) + return + } if let audioB64 = (obj["data"] as? String), let audioData = Data(base64Encoded: audioB64) { recvAudio.append(audioData) processAudioChunk(incoming: audioData, multiple: 1280) diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/Info.plist b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/Info.plist index db1458689..ba249d11c 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/Info.plist +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/Info.plist @@ -18,6 +18,21 @@ SupportedPlatform ios + + BinaryPath + JLBmpConvertKit.framework/Versions/A/JLBmpConvertKit + LibraryIdentifier + macos-arm64_x86_64 + LibraryPath + JLBmpConvertKit.framework + SupportedArchitectures + + arm64 + x86_64 + + SupportedPlatform + macos + BinaryPath JLBmpConvertKit.framework/JLBmpConvertKit diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64/JLBmpConvertKit.framework/Info.plist b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64/JLBmpConvertKit.framework/Info.plist index 1c45c385b..850885dce 100644 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64/JLBmpConvertKit.framework/Info.plist and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64/JLBmpConvertKit.framework/Info.plist differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64/JLBmpConvertKit.framework/JLBmpConvertKit b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64/JLBmpConvertKit.framework/JLBmpConvertKit index bdd0bf9f0..587d9cb6f 100755 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64/JLBmpConvertKit.framework/JLBmpConvertKit and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64/JLBmpConvertKit.framework/JLBmpConvertKit differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64_x86_64-simulator/JLBmpConvertKit.framework/Info.plist b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64_x86_64-simulator/JLBmpConvertKit.framework/Info.plist index 4409cd0e0..6296ca17e 100644 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64_x86_64-simulator/JLBmpConvertKit.framework/Info.plist and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64_x86_64-simulator/JLBmpConvertKit.framework/Info.plist differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64_x86_64-simulator/JLBmpConvertKit.framework/JLBmpConvertKit b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64_x86_64-simulator/JLBmpConvertKit.framework/JLBmpConvertKit index e58f37985..0566cc69e 100755 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64_x86_64-simulator/JLBmpConvertKit.framework/JLBmpConvertKit and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64_x86_64-simulator/JLBmpConvertKit.framework/JLBmpConvertKit differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64_x86_64-simulator/JLBmpConvertKit.framework/_CodeSignature/CodeResources b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64_x86_64-simulator/JLBmpConvertKit.framework/_CodeSignature/CodeResources index 7b50e6cc4..c9afe2ae9 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64_x86_64-simulator/JLBmpConvertKit.framework/_CodeSignature/CodeResources +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64_x86_64-simulator/JLBmpConvertKit.framework/_CodeSignature/CodeResources @@ -26,7 +26,7 @@ Info.plist - mH2wyDJH099CSL7ndYKtKlBiJ3M= + yF0Q5ofGeBGl7TtNvUrICUVzXe8= Modules/module.modulemap diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Headers b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Headers new file mode 120000 index 000000000..a177d2a6b --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Headers @@ -0,0 +1 @@ +Versions/Current/Headers \ No newline at end of file diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/JLBmpConvertKit b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/JLBmpConvertKit new file mode 120000 index 000000000..3f7bc01c8 --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/JLBmpConvertKit @@ -0,0 +1 @@ +Versions/Current/JLBmpConvertKit \ No newline at end of file diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Modules b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Modules new file mode 120000 index 000000000..5736f3186 --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Modules @@ -0,0 +1 @@ +Versions/Current/Modules \ No newline at end of file diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Resources b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Resources new file mode 120000 index 000000000..953ee36f3 --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Resources @@ -0,0 +1 @@ +Versions/Current/Resources \ No newline at end of file diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLBmpConvert.h b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLBmpConvert.h new file mode 100644 index 000000000..1d20bffaa --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLBmpConvert.h @@ -0,0 +1,95 @@ +// +// JLBmpCovert.h +// JLBmpCovertKit +// +// Created by EzioChan on 2024/9/9. +// Copyright © 2024 www.zh-jieli.com. All rights reserved. +// + +#import +#import +#if TARGET_OS_IOS || TARGET_OS_MACCATALYST +#import +#elif TARGET_OS_OSX +#import +#endif + + +NS_ASSUME_NONNULL_BEGIN +@class JLImageConvertResult; + +typedef void(^JLBmpCovertCallBack)(NSString *inFilePath,NSString *__nullable outFilePath,NSError *__nullable error) __deprecated_msg("This interface is deprecated. Please use convert:inFilePath:outFilePath: or convert:Image: instead."); + +typedef void(^JLBmpCovertCallBack2)(NSData *__nullable outFileData,NSError *__nullable error) __deprecated_msg("This interface is deprecated. Please use convert:inFilePath:outFilePath: or convert:Image: instead."); + + +/// 图像转换 +@interface JLBmpConvert : NSObject +#if TARGET_OS_IOS || TARGET_OS_MACCATALYST + +/// 图片转换 +/// - Parameters: +/// - type: 转换类型 +/// - inFilePath: 输入文件 +/// - outFilePath: 输出文件 +/// - completion: 回调 ++(void)covert:(JLBmpConvertType)type + inFilePath:(NSString *_Nonnull)inFilePath + outFilePath:(NSString * __nullable)outFilePath + completion:(JLBmpCovertCallBack _Nonnull)completion __deprecated_msg("This interface is deprecated. Please use convert:inFilePath:outFilePath: or convert:Image: instead."); + +/// 图片转换 +/// - Parameters: +/// - type: 转换类型 +/// - inImage: 输入图片 +/// - completion: 回调 ++(void)covert:(JLBmpConvertType)type + Image:(UIImage *)inImage + completion:(JLBmpCovertCallBack2 _Nonnull)completion __deprecated_msg("This interface is deprecated. Please use convert:inFilePath:outFilePath: or convert:Image: instead."); + +/// 图片转换 +/// - Parameters: +/// - option: 类型 +/// 当前仅仅 707N 系列芯片可指定图像格式 +/// - inFilePath: 输入文件 +/// - outFilePath: 输出文件 +/// 文件路径转换入口 ++ (JLImageConvertResult *)convert:(JLBmpConvertOption *)option + inFilePath:(NSString *)inFilePath + outFilePath:(NSString * __nullable)outFilePath; + +/// UIImage 转换 +/// 当前仅仅 707N 系列芯片可指定图像格式 +/// - Parameters: +/// - option: 类型 +/// - imgData: 图片的原始数据 ++(JLImageConvertResult *)convert:(JLBmpConvertOption *)option ImageData:(NSData *)imgData; + + +/// UIImage 转换成 BGRA +/// - Parameter image: 图片 ++(NSData*)convertImageToBGRABytes:(UIImage *)image; + +/// UIImage 转换成 RGBA +/// - Parameter image: 图片 ++(NSData*)convertImageToRGBABytes:(UIImage *)image; + + +/// 图片缩放 +/// - Parameters: +/// - image: 图片 +/// - newSize: 缩放大小 +/// - Returns: 缩放后的图片 ++(NSData *)resizeImage:(UIImage*)image andResizeTo:(CGSize)newSize; + +#elif TARGET_OS_OSX +/// 图片转换 +/// - Parameters: +/// - option: 转换类型 +/// - inImage: 输入图片 ++(JLImageConvertResult *)convert:(JLBmpConvertOption *)option Image:(NSImage *)image; + +#endif +@end + +NS_ASSUME_NONNULL_END diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLBmpConvertKit.h b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLBmpConvertKit.h new file mode 100644 index 000000000..9ff73bf13 --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLBmpConvertKit.h @@ -0,0 +1,24 @@ +// +// JLBmpConvertKit.h +// JLBmpConvertKit +// +// Created by EzioChan on 2024/9/9. +// Copyright © 2024 www.zh-jieli.com. All rights reserved. +// + +#import + +//! Project version number for JLBmpConvertKit. +FOUNDATION_EXPORT double JLBmpConvertKitVersionNumber; + +//! Project version string for JLBmpConvertKit. +FOUNDATION_EXPORT const unsigned char JLBmpConvertKitVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + +#import +#import +#import +#import + diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLBmpConvertOption.h b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLBmpConvertOption.h new file mode 100644 index 000000000..41e05510c --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLBmpConvertOption.h @@ -0,0 +1,89 @@ +// +// JLBmpConvertOption.h +// JLBmpConvertKit +// +// Created by EzioChan on 2025/5/23. +// Copyright © 2025 www.zh-jieli.com. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// 图像转换类型 +typedef NS_ENUM(NSUInteger, JLBmpConvertType) { + /// 695N 图像转换 RGB 转换成 695N 芯片对应的图像资源 + /// 对应原 BR23 转换算法 + JLBmpConvertType695N_RBG = 0, + /// 701N 图像转换 RGB 转换成 701N 芯片对应的图像资源 + /// 对应原 BR28 转换的算法 + JLBmpConvertType701N_RBG = 1, + /// 701N 图像转换 ARGB 转换成 701N 芯片对应的图像资源 + /// 对应原 BR28_ARBG 转换的算法 + JLBmpConvertType701N_ARBG = 2, + /// 701N 图像转换 RGB 转换成 701N 芯片对应的图像资源(不打包封装) + /// 对应原 BR28_RGB_NO_PACK 转换的算法(不打包封装) + JLBmpConvertType701N_RBG_NO_PACK = 3, + /// 701N 图像转换 ARGB 转换成 701N 芯片对应的图像资源(不打包封装) + /// 对应原 BR28_ARGB_NO_PACK 转换的算法(不打包封装) + JLBmpConvertType701N_ARGB_NO_PACK = 4, + /// 707N 图像转换 RGB 转换成 707N 芯片对应的图像资源 + /// 对应 BR35 转换的算法 + JLBmpConvertType707N_RBG = 5, + /// 707N 图像转换 ARGB 转换成 707N 芯片对应的图像资源 + /// 对应 BR35_ARGB 转换的算法 + JLBmpConvertType707N_ARGB = 6, + /// 707N 图像转换 RGB 转换成 707N 芯片对应的图像资源(不打包封装) + /// 对应 BR35_RGB_NO_PACK 转换的算法(不打包封装) + JLBmpConvertType707N_ARGB_NO_PACK = 7, + /// 707N 图像转换 RGB 转换成 707N 芯片对应的图像资源(不打包封装) + /// 对应 BR35_RGB_NO_PACK 转换的算法(不打包封装) + JLBmpConvertType707N_RBG_NO_PACK = 8, + /// 701N 图像转换 JPEG + /// 仅仅支持 JPEG 类型 + JLBmpConvertType701N_JPEG = 9 +}; + +/// 图像格式 +/// 仅(707N 系列芯片支持) +typedef NS_ENUM(NSUInteger, JLBmpPixelformat) { + /// ARBG8888/RBG888 格式 + JLBmpPixelformat_888, + /// ARBG8565/RBG565 格式 + JLBmpPixelformat_565, + /// 自动选择最小的格式 + JLBmpPixelformat_Auto, +}; + +/// LVGL 打包格式 +typedef NS_ENUM(NSUInteger, JLBmpPacketFormat) { + JLBmpPacketFormatNone = 0, + JLBmpPacketFormatJLUI = 1, + JLBmpPacketFormatLVGL = 2 +}; + +/// 图像转换选项 +@interface JLBmpConvertOption : NSObject + +/// 图像转换类型 +@property (nonatomic, assign) JLBmpConvertType convertType; + +/// 图像格式 +/// 默认为 auto +/// 仅(707N 系列芯片支持) +@property (nonatomic, assign) JLBmpPixelformat pixelformat; + +/// LVGL 打包格式 +/// 默认为 JLUI +/// 仅(707N 系列芯片支持) +@property (nonatomic, assign) JLBmpPacketFormat packetFormat; + +/// 是否转换成 BGRA +/// 当使用的是 JL 的 UI 框架时需要使用 +/// 使用其他第三方框架时,需要根据固件端框架进行调整 +/// default YES +@property (nonatomic, assign) BOOL convertToBGRA; + +@end + +NS_ASSUME_NONNULL_END diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLGifBin.h b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLGifBin.h new file mode 100644 index 000000000..5b7cdc2d7 --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLGifBin.h @@ -0,0 +1,77 @@ +// +// JLGifBin.h +// JLGifLib +// +// Created by EzioChan on 2024/1/18. +// Copyright © 2024 www.zh-jieli.com. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// Gif 转 Bin 支持的芯片类型 +typedef NS_ENUM(NSUInteger, JLGIFBinChipType) { + /// 701N + JLGIFBinChipJL_701N, + /// 707N + JLGIFBinChipJL_707N, +}; + +///packet 是否打包 +typedef NS_ENUM(NSInteger, JLGIFBinPackageType) { + /// 不做其他打包使用源数据,适用 LVGL 的公版UI框架 + JLGIFBinPackageTypeLVGL = 0, + /// 杰理打包格式,适用 JLVGL 的杰理定制版 UI 框架 + JLGIFBinPackageTypeJLVGL = 1, +}; + +typedef void(^JLGif2BinBlock)(int code,NSData *_Nullable binData); + +/// Gif 转 Bin +@interface JLGifBin : NSObject + +/// 打印当前SDK的版本 ++(void)sdkVersion; + +/// 根据 Gif 创建 Bin +/// - Parameters: +/// - gifData: Gif 图片数据 +/// - level: 等级 +/// 1: 低码率 +/// 2: 中码率 +/// 3: 高码率 +/// - block: 回调内容 ++(void)makeDataToBin:(NSData *)gifData Level:(int)level Result:(JLGif2BinBlock)block __attribute__((deprecated("Use makeDataToBin:Level:ChipType:Result: instead."))); + + +/// 根据 Gif 创建 Bin +/// - Parameters: +/// - gifData: Gif 图片数据 +/// - level: 等级 +/// 1: 低码率 +/// 2: 中码率 (仅 JLGIFBinChipJL_701N 支持) +/// 3: 高码率 (仅 JLGIFBinChipJL_701N 支持) +/// - chip: 芯片类型 +/// - block: 回调内容 ++(void)makeDataToBin:(NSData *)gifData Level:(int)level ChipType:(JLGIFBinChipType)chip Result:(JLGif2BinBlock)block __attribute__((deprecated("Use makeDataToBin:Level:ChipType:PackageType:Result: instead."))); + + +/// 根据 Gif 创建 Bin +/// - Parameters: +/// - gifData: Gif 图片数据 +/// - level: 等级 +/// 1: 低码率 +/// 2: 中码率 (仅 JLGIFBinChipJL_701N 支持) +/// 3: 高码率 (仅 JLGIFBinChipJL_701N 支持) +/// - chip: 芯片类型 +/// - packageType: 打包类型 +/// 1: JLUI 杰理 UI 格式 +/// 0: 无 不打包 +/// - block: 回调内容 ++(void)makeDataToBin:(NSData *)gifData Level:(int)level ChipType:(JLGIFBinChipType)chip PackageType:(JLGIFBinPackageType)packageType Result:(JLGif2BinBlock)block; + + +@end + +NS_ASSUME_NONNULL_END diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLImageConvertResult.h b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLImageConvertResult.h new file mode 100644 index 000000000..0180e5784 --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLImageConvertResult.h @@ -0,0 +1,34 @@ +// +// JLImageConvertResult.h +// JLBmpConvertKit +// +// Created by EzioChan on 2025/3/6. +// Copyright © 2025 www.zh-jieli.com. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// 图像转换结果 +@interface JLImageConvertResult : NSObject +/// 结果码 +@property (nonatomic, assign) int result; +/// 数据大小 +@property (nonatomic, assign) int buf_size; +/// 图像格式 +@property (nonatomic, assign) int pixel_format; +/// 压缩模式 +@property (nonatomic, assign) int compress_mode; + +#if TARGET_OS_IOS || TARGET_OS_MACCATALYST +/// 输出文件路径 +@property (nullable, nonatomic, copy) NSString *outFilePath; +#endif + +/// 输出文件内容 +@property (nullable, nonatomic, copy) NSData *outFileData; + +@end + +NS_ASSUME_NONNULL_END diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/JLBmpConvertKit b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/JLBmpConvertKit new file mode 100755 index 000000000..700cfacc9 Binary files /dev/null and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/JLBmpConvertKit differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Modules/module.modulemap b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Modules/module.modulemap new file mode 100644 index 000000000..b3d18e590 --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Modules/module.modulemap @@ -0,0 +1,6 @@ +framework module JLBmpConvertKit { + umbrella header "JLBmpConvertKit.h" + export * + + module * { export * } +} diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Resources/Info.plist b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Resources/Info.plist new file mode 100644 index 000000000..038498a78 --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Resources/Info.plist @@ -0,0 +1,48 @@ + + + + + BuildMachineOSBuild + 25B78 + CFBundleDevelopmentRegion + en + CFBundleExecutable + JLBmpConvertKit + CFBundleIdentifier + com.zh-jieli.JLBmpConvertKit + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + JLBmpConvertKit + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.5.0 + CFBundleSupportedPlatforms + + MacOSX + + CFBundleVersion + 1 + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 25C57 + DTPlatformName + macosx + DTPlatformVersion + 26.2 + DTSDKBuild + 25C57 + DTSDKName + macosx26.2 + DTXcode + 2620 + DTXcodeBuild + 17C52 + LSMinimumSystemVersion + 10.13 + NSHumanReadableCopyright + Copyright © 2024 www.zh-jieli.com. All rights reserved. + + diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/Current b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/Current new file mode 120000 index 000000000..8c7e5a667 --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/Current @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64/JLDialUnit.framework/Info.plist b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64/JLDialUnit.framework/Info.plist index effe2c50a..e883516d0 100644 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64/JLDialUnit.framework/Info.plist and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64/JLDialUnit.framework/Info.plist differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64/JLDialUnit.framework/JLDialUnit b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64/JLDialUnit.framework/JLDialUnit index 12ada05cd..66c880b98 100755 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64/JLDialUnit.framework/JLDialUnit and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64/JLDialUnit.framework/JLDialUnit differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64_x86_64-simulator/JLDialUnit.framework/Info.plist b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64_x86_64-simulator/JLDialUnit.framework/Info.plist index 83e1ad523..e8a73fc24 100644 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64_x86_64-simulator/JLDialUnit.framework/Info.plist and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64_x86_64-simulator/JLDialUnit.framework/Info.plist differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64_x86_64-simulator/JLDialUnit.framework/JLDialUnit b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64_x86_64-simulator/JLDialUnit.framework/JLDialUnit index 3bff2f486..48eb602dc 100755 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64_x86_64-simulator/JLDialUnit.framework/JLDialUnit and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64_x86_64-simulator/JLDialUnit.framework/JLDialUnit differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64_x86_64-simulator/JLDialUnit.framework/_CodeSignature/CodeResources b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64_x86_64-simulator/JLDialUnit.framework/_CodeSignature/CodeResources index 84c4f3a0e..2e8376acb 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64_x86_64-simulator/JLDialUnit.framework/_CodeSignature/CodeResources +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64_x86_64-simulator/JLDialUnit.framework/_CodeSignature/CodeResources @@ -50,7 +50,7 @@ Info.plist - S01R8qKENWyeFMKwZu8s9ypBNCQ= + UOVQW+J5hxFv4GqeGxCjfcQ+XtE= Modules/module.modulemap diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/Info.plist b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/Info.plist index 6ce64f21d..1d5ad85ad 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/Info.plist +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/Info.plist @@ -4,6 +4,21 @@ AvailableLibraries + + BinaryPath + JLLogHelper.framework/Versions/A/JLLogHelper + LibraryIdentifier + macos-arm64_x86_64 + LibraryPath + JLLogHelper.framework + SupportedArchitectures + + arm64 + x86_64 + + SupportedPlatform + macos + BinaryPath JLLogHelper.framework/JLLogHelper diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64/JLLogHelper.framework/Info.plist b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64/JLLogHelper.framework/Info.plist index 3c961562c..a4ba86f07 100644 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64/JLLogHelper.framework/Info.plist and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64/JLLogHelper.framework/Info.plist differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64/JLLogHelper.framework/JLLogHelper b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64/JLLogHelper.framework/JLLogHelper index 7d4cce100..6c97bde0c 100755 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64/JLLogHelper.framework/JLLogHelper and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64/JLLogHelper.framework/JLLogHelper differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64_x86_64-simulator/JLLogHelper.framework/Info.plist b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64_x86_64-simulator/JLLogHelper.framework/Info.plist index b8d6b7f94..3a5d81280 100644 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64_x86_64-simulator/JLLogHelper.framework/Info.plist and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64_x86_64-simulator/JLLogHelper.framework/Info.plist differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64_x86_64-simulator/JLLogHelper.framework/JLLogHelper b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64_x86_64-simulator/JLLogHelper.framework/JLLogHelper index df8c2858b..4289a0bb2 100755 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64_x86_64-simulator/JLLogHelper.framework/JLLogHelper and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64_x86_64-simulator/JLLogHelper.framework/JLLogHelper differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64_x86_64-simulator/JLLogHelper.framework/_CodeSignature/CodeResources b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64_x86_64-simulator/JLLogHelper.framework/_CodeSignature/CodeResources index 1ae81019b..8ac50a58c 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64_x86_64-simulator/JLLogHelper.framework/_CodeSignature/CodeResources +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64_x86_64-simulator/JLLogHelper.framework/_CodeSignature/CodeResources @@ -14,7 +14,7 @@ Info.plist - PJl+BYy7qbdneBUnydYicJKb/SM= + mJ09lIigUxIgwpwz1tMqaHs8roI= Modules/module.modulemap diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Headers b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Headers new file mode 120000 index 000000000..a177d2a6b --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Headers @@ -0,0 +1 @@ +Versions/Current/Headers \ No newline at end of file diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/JLLogHelper b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/JLLogHelper new file mode 120000 index 000000000..c49bd284e --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/JLLogHelper @@ -0,0 +1 @@ +Versions/Current/JLLogHelper \ No newline at end of file diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Modules b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Modules new file mode 120000 index 000000000..5736f3186 --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Modules @@ -0,0 +1 @@ +Versions/Current/Modules \ No newline at end of file diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Resources b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Resources new file mode 120000 index 000000000..953ee36f3 --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Resources @@ -0,0 +1 @@ +Versions/Current/Resources \ No newline at end of file diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/Headers/JLLogHelper.h b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/Headers/JLLogHelper.h new file mode 100644 index 000000000..a681462cc --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/Headers/JLLogHelper.h @@ -0,0 +1,19 @@ +// +// JLLogHelper.h +// JLLogHelper +// +// Created by EzioChan on 2024/5/8. +// Copyright © 2024 www.zh-jieli.com. All rights reserved. +// + +#import + +//! Project version number for JLLogHelper. +FOUNDATION_EXPORT double JLLogHelperVersionNumber; + +//! Project version string for JLLogHelper. +FOUNDATION_EXPORT const unsigned char JLLogHelperVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + +#import diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/Headers/JLLogManager.h b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/Headers/JLLogManager.h new file mode 100644 index 000000000..8f16c2a12 --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/Headers/JLLogManager.h @@ -0,0 +1,88 @@ +// +// JLLogManager.h +// JLLogHelper +// +// Created by EzioChan on 2024/5/8. +// Copyright © 2024 www.zh-jieli.com. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// 打印宏 +#define kJLLog(level,fmt...) [JLLogManager logLevel:level funcName:__FUNCTION__ line:__LINE__ format:fmt] + +typedef NS_ENUM(NSInteger, JLLOG_LEVEL) { + JLLOG_COMPLETE = 0, + JLLOG_DEBUG = 1, + JLLOG_INFO = 2, + JLLOG_WARN = 3, + JLLOG_ERROR = 4, +}; + +/// 日志打印 +@interface JLLogManager : NSObject + +/// 打印SDK版本 ++(void)sdkVersion; + +///清理已存储的打印 ++(void)clearLog; + +/** + * 打印文本 + */ ++(void)openLogTextFile; + +///重定向保存日志文件路径设置 +/// @param path 路径 ++(void)redirectLogPath:(NSString *)path; + +/// 收集日志回调接口 +/// @param block 回调 ++(void)collectLog:(void(^)(NSString *log))block; + +/// 打印时间 +/// @param isTimeStamp 是否打印 default is false ++(void)logWithTimestamp:(BOOL)isTimeStamp; + +/// 把日志保存存储起来 +/// @param isSave 是否保存 default is yes ++(void)saveLogAsFile:(BOOL)isSave; + +///LOG使能与等级,默认开启且debug等级。 +/// @param enable LOG使能 +/// @param isMore 是否打印【函数名&行号】 +/// @param level LOG等级 ++(void)setLog:(BOOL)enable IsMore:(BOOL)isMore Level:(JLLOG_LEVEL)level; + +/// 打印函数 +/// @param level LOG等级 +/// @param func 函数名 +/// @param line 行号 +/// @param format 内容 ++(void)logLevel:(JLLOG_LEVEL)level funcName:(const char* _Nullable)func line:(const int)line format:(NSString * _Nonnull)format,...; + +/// 打印 +/// @param something 任意字符串内容 ++(void)logSomething:(NSString *)something; + +/// 打印对象 +/// @param object 任意对象 ++(void)logObject:(id)object; + + + +/// 打印 +/// @param level LOG等级 +/// @param content 内容 ++(void)logLevel:(JLLOG_LEVEL)level content:(NSString *)content; + +/// 保存日志信息到文件中 +/// @param logContent 日志内容 ++(void)saveLog:(NSString *)logContent; + +@end + +NS_ASSUME_NONNULL_END diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/JLLogHelper b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/JLLogHelper new file mode 100755 index 000000000..fe6317d02 Binary files /dev/null and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/JLLogHelper differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/Modules/module.modulemap b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/Modules/module.modulemap new file mode 100644 index 000000000..21384c4ed --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/Modules/module.modulemap @@ -0,0 +1,6 @@ +framework module JLLogHelper { + umbrella header "JLLogHelper.h" + export * + + module * { export * } +} diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/Resources/Info.plist b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/Resources/Info.plist new file mode 100644 index 000000000..2641e5283 --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/Resources/Info.plist @@ -0,0 +1,48 @@ + + + + + BuildMachineOSBuild + 25B78 + CFBundleDevelopmentRegion + en + CFBundleExecutable + JLLogHelper + CFBundleIdentifier + com.zh-jieli.JLLogHelper + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + JLLogHelper + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.2 + CFBundleSupportedPlatforms + + MacOSX + + CFBundleVersion + 1 + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 25C57 + DTPlatformName + macosx + DTPlatformVersion + 26.2 + DTSDKBuild + 25C57 + DTSDKName + macosx26.2 + DTXcode + 2620 + DTXcodeBuild + 17C52 + LSMinimumSystemVersion + 26.2 + NSHumanReadableCopyright + Copyright © 2024 www.zh-jieli.com. All rights reserved. + + diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/Current b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/Current new file mode 120000 index 000000000..8c7e5a667 --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/Current @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLPackageResKit.xcframework/ios-arm64/JLPackageResKit.framework/JLPackageResKit b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLPackageResKit.xcframework/ios-arm64/JLPackageResKit.framework/JLPackageResKit index fc3c89793..0116faefb 100755 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLPackageResKit.xcframework/ios-arm64/JLPackageResKit.framework/JLPackageResKit and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLPackageResKit.xcframework/ios-arm64/JLPackageResKit.framework/JLPackageResKit differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLPackageResKit.xcframework/ios-arm64_x86_64-simulator/JLPackageResKit.framework/JLPackageResKit b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLPackageResKit.xcframework/ios-arm64_x86_64-simulator/JLPackageResKit.framework/JLPackageResKit index 996ef7cc4..d01bc546f 100755 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLPackageResKit.xcframework/ios-arm64_x86_64-simulator/JLPackageResKit.framework/JLPackageResKit and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JLPackageResKit.xcframework/ios-arm64_x86_64-simulator/JLPackageResKit.framework/JLPackageResKit differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/Info.plist b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/Info.plist index ac38ab92b..4fcd4233a 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/Info.plist +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/Info.plist @@ -8,32 +8,32 @@ BinaryPath JL_AdvParse.framework/JL_AdvParse LibraryIdentifier - ios-arm64 + ios-arm64_x86_64-simulator LibraryPath JL_AdvParse.framework SupportedArchitectures arm64 + x86_64 SupportedPlatform ios + SupportedPlatformVariant + simulator BinaryPath JL_AdvParse.framework/JL_AdvParse LibraryIdentifier - ios-arm64_x86_64-simulator + ios-arm64 LibraryPath JL_AdvParse.framework SupportedArchitectures arm64 - x86_64 SupportedPlatform ios - SupportedPlatformVariant - simulator CFBundlePackageType diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64/JL_AdvParse.framework/Info.plist b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64/JL_AdvParse.framework/Info.plist index 7f060a4a3..06d80536a 100644 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64/JL_AdvParse.framework/Info.plist and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64/JL_AdvParse.framework/Info.plist differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64/JL_AdvParse.framework/JL_AdvParse b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64/JL_AdvParse.framework/JL_AdvParse index 9c04419b0..e05e588f5 100755 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64/JL_AdvParse.framework/JL_AdvParse and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64/JL_AdvParse.framework/JL_AdvParse differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64_x86_64-simulator/JL_AdvParse.framework/Info.plist b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64_x86_64-simulator/JL_AdvParse.framework/Info.plist index 284ba59ec..fbab0bff6 100644 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64_x86_64-simulator/JL_AdvParse.framework/Info.plist and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64_x86_64-simulator/JL_AdvParse.framework/Info.plist differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64_x86_64-simulator/JL_AdvParse.framework/JL_AdvParse b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64_x86_64-simulator/JL_AdvParse.framework/JL_AdvParse index 27d3e726d..fb68b813c 100755 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64_x86_64-simulator/JL_AdvParse.framework/JL_AdvParse and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64_x86_64-simulator/JL_AdvParse.framework/JL_AdvParse differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64_x86_64-simulator/JL_AdvParse.framework/_CodeSignature/CodeResources b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64_x86_64-simulator/JL_AdvParse.framework/_CodeSignature/CodeResources index e2beeb0cb..f3d7ac375 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64_x86_64-simulator/JL_AdvParse.framework/_CodeSignature/CodeResources +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64_x86_64-simulator/JL_AdvParse.framework/_CodeSignature/CodeResources @@ -46,7 +46,7 @@ Info.plist - y32GLYYZ4FDhJGOjUkYUZfxaRn8= + Yw+NwkpNMafTbGDLvF8mhT3Alh8= Modules/module.modulemap diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64/JL_BLEKit.framework/Headers/JLDialInfoExtentedModel.h b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64/JL_BLEKit.framework/Headers/JLDialInfoExtentedModel.h index 9b516dc8b..f5c51807c 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64/JL_BLEKit.framework/Headers/JLDialInfoExtentedModel.h +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64/JL_BLEKit.framework/Headers/JLDialInfoExtentedModel.h @@ -42,6 +42,29 @@ NS_ASSUME_NONNULL_BEGIN /// device screen sice @property(nonatomic, assign) CGSize size; +/// 是否支持多媒体(视频/GIF) +/// is support medias +@property(nonatomic, assign) BOOL isSupportMedias; + +/// 最大帧率(当支持多媒体时才有意义) +/// max fps +@property(nonatomic, assign) NSInteger maxFPS; + +/// 最大时长 +/// max duration +@property(nonatomic, assign) NSInteger maxDuration; + +/// 是否支持动画 ani 格式 +/// is support animation +@property(nonatomic, assign) BOOL isSupportAni; + +/// 是否支持视频 avi 格式 +@property(nonatomic, assign) BOOL isSupportAvi; + +/// 是否支持 Gif 格式 +/// is support gif +@property(nonatomic, assign) BOOL isSupportGif; + /// 初始化 /// initialize diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64/JL_BLEKit.framework/Headers/JL_Assist.h b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64/JL_BLEKit.framework/Headers/JL_Assist.h index 6cdb50fa5..15453d0b9 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64/JL_BLEKit.framework/Headers/JL_Assist.h +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64/JL_BLEKit.framework/Headers/JL_Assist.h @@ -35,6 +35,7 @@ typedef void(^JL_Assist_BK)(BOOL isPaired); @property(assign,nonatomic)BOOL mAuthEnable; @property(assign,nonatomic)BOOL mLogData; //是否打印裸数据 @property(assign,nonatomic)NSInteger mLimitMtu; //(默认40)在最大的MTU基础上减少数据量 +@property(assign,nonatomic)NSInteger mMaxMtu; //最大MTU(初始化不一定准确,可以再实际使用时再次获取一下)[peripheral maximumWriteValueLengthForType:CBCharacteristicWriteWithoutResponse]; @property(strong,nonatomic)CBPeripheral *__nullable mRcspPeripheral; @property(strong,nonatomic)CBCharacteristic *__nullable mRcspWrite; @property(strong,nonatomic)CBCharacteristic *__nullable mRcspRead; diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64/JL_BLEKit.framework/JL_BLEKit b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64/JL_BLEKit.framework/JL_BLEKit index 1a618c3c3..3bebb0940 100755 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64/JL_BLEKit.framework/JL_BLEKit and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64/JL_BLEKit.framework/JL_BLEKit differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/Headers/JLDialInfoExtentedModel.h b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/Headers/JLDialInfoExtentedModel.h index 9b516dc8b..f5c51807c 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/Headers/JLDialInfoExtentedModel.h +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/Headers/JLDialInfoExtentedModel.h @@ -42,6 +42,29 @@ NS_ASSUME_NONNULL_BEGIN /// device screen sice @property(nonatomic, assign) CGSize size; +/// 是否支持多媒体(视频/GIF) +/// is support medias +@property(nonatomic, assign) BOOL isSupportMedias; + +/// 最大帧率(当支持多媒体时才有意义) +/// max fps +@property(nonatomic, assign) NSInteger maxFPS; + +/// 最大时长 +/// max duration +@property(nonatomic, assign) NSInteger maxDuration; + +/// 是否支持动画 ani 格式 +/// is support animation +@property(nonatomic, assign) BOOL isSupportAni; + +/// 是否支持视频 avi 格式 +@property(nonatomic, assign) BOOL isSupportAvi; + +/// 是否支持 Gif 格式 +/// is support gif +@property(nonatomic, assign) BOOL isSupportGif; + /// 初始化 /// initialize diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/Headers/JL_Assist.h b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/Headers/JL_Assist.h index 6cdb50fa5..15453d0b9 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/Headers/JL_Assist.h +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/Headers/JL_Assist.h @@ -35,6 +35,7 @@ typedef void(^JL_Assist_BK)(BOOL isPaired); @property(assign,nonatomic)BOOL mAuthEnable; @property(assign,nonatomic)BOOL mLogData; //是否打印裸数据 @property(assign,nonatomic)NSInteger mLimitMtu; //(默认40)在最大的MTU基础上减少数据量 +@property(assign,nonatomic)NSInteger mMaxMtu; //最大MTU(初始化不一定准确,可以再实际使用时再次获取一下)[peripheral maximumWriteValueLengthForType:CBCharacteristicWriteWithoutResponse]; @property(strong,nonatomic)CBPeripheral *__nullable mRcspPeripheral; @property(strong,nonatomic)CBCharacteristic *__nullable mRcspWrite; @property(strong,nonatomic)CBCharacteristic *__nullable mRcspRead; diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/JL_BLEKit b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/JL_BLEKit index 9e7d924c7..02cceaeb5 100755 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/JL_BLEKit and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/JL_BLEKit differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/_CodeSignature/CodeResources b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/_CodeSignature/CodeResources index 729777c87..63971ab00 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/_CodeSignature/CodeResources +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/_CodeSignature/CodeResources @@ -110,7 +110,7 @@ Headers/JLDialInfoExtentedModel.h - SqF+gwV8xas+qeqllpe46CC0G0k= + u05R1xDr/Goqgu1SLpflf0Okryc= Headers/JLDisconnectRemindModel.h @@ -338,7 +338,7 @@ Headers/JL_Assist.h - JmuMMHFAyKbie1xtZpOhO3PEoxk= + JmeqEfLSOWlrtfMvA3oj2ZB4vlA= Headers/JL_BLEAction.h @@ -861,11 +861,11 @@ hash - SqF+gwV8xas+qeqllpe46CC0G0k= + u05R1xDr/Goqgu1SLpflf0Okryc= hash2 - tXTQgWqZ6OBex+aPNIsDWzI2fMA80L9JXEwmv8JeEao= + dLH0g5OaY6mg+cEo6jDF9a/xSBcoI/Tt+Ukmbk+2cJE= Headers/JLDisconnectRemindModel.h @@ -1488,11 +1488,11 @@ hash - JmuMMHFAyKbie1xtZpOhO3PEoxk= + JmeqEfLSOWlrtfMvA3oj2ZB4vlA= hash2 - J2dFYlibDG6vMXH366nZsKEAuJyWWFT5POxY7X9txWM= + 6QbcztusftVvRuOkEEH+xwPBC+QfHfVjy2JNe4WUOIM= Headers/JL_BLEAction.h diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64/JL_HashPair.framework/Info.plist b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64/JL_HashPair.framework/Info.plist index fdbde3775..232ca4a24 100644 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64/JL_HashPair.framework/Info.plist and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64/JL_HashPair.framework/Info.plist differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64/JL_HashPair.framework/JL_HashPair b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64/JL_HashPair.framework/JL_HashPair index fe3eb3303..bb02d2d6f 100755 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64/JL_HashPair.framework/JL_HashPair and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64/JL_HashPair.framework/JL_HashPair differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64_x86_64-simulator/JL_HashPair.framework/Info.plist b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64_x86_64-simulator/JL_HashPair.framework/Info.plist index 4d99eceaf..1bc046944 100644 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64_x86_64-simulator/JL_HashPair.framework/Info.plist and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64_x86_64-simulator/JL_HashPair.framework/Info.plist differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64_x86_64-simulator/JL_HashPair.framework/JL_HashPair b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64_x86_64-simulator/JL_HashPair.framework/JL_HashPair index be2445248..f2c842955 100755 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64_x86_64-simulator/JL_HashPair.framework/JL_HashPair and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64_x86_64-simulator/JL_HashPair.framework/JL_HashPair differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64_x86_64-simulator/JL_HashPair.framework/_CodeSignature/CodeResources b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64_x86_64-simulator/JL_HashPair.framework/_CodeSignature/CodeResources index 9022691a5..ae6ca022a 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64_x86_64-simulator/JL_HashPair.framework/_CodeSignature/CodeResources +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64_x86_64-simulator/JL_HashPair.framework/_CodeSignature/CodeResources @@ -22,7 +22,7 @@ Info.plist - pLA2PfPyxsguwM09KFXPq/Cy2ko= + vaP94eof9P4dJHy/VxAhSfezsXw= Modules/module.modulemap diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64/JL_OTALib.framework/Info.plist b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64/JL_OTALib.framework/Info.plist index 6ca8a27f2..c26c4dc80 100644 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64/JL_OTALib.framework/Info.plist and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64/JL_OTALib.framework/Info.plist differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64/JL_OTALib.framework/JL_OTALib b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64/JL_OTALib.framework/JL_OTALib index 45b500d0f..65d4ac78a 100755 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64/JL_OTALib.framework/JL_OTALib and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64/JL_OTALib.framework/JL_OTALib differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64_x86_64-simulator/JL_OTALib.framework/Info.plist b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64_x86_64-simulator/JL_OTALib.framework/Info.plist index e118ebce9..6618e678c 100644 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64_x86_64-simulator/JL_OTALib.framework/Info.plist and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64_x86_64-simulator/JL_OTALib.framework/Info.plist differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64_x86_64-simulator/JL_OTALib.framework/JL_OTALib b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64_x86_64-simulator/JL_OTALib.framework/JL_OTALib index 23599f86d..5c9ec35d6 100755 Binary files a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64_x86_64-simulator/JL_OTALib.framework/JL_OTALib and b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64_x86_64-simulator/JL_OTALib.framework/JL_OTALib differ diff --git a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64_x86_64-simulator/JL_OTALib.framework/_CodeSignature/CodeResources b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64_x86_64-simulator/JL_OTALib.framework/_CodeSignature/CodeResources index 233cf6377..c5d8627c8 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64_x86_64-simulator/JL_OTALib.framework/_CodeSignature/CodeResources +++ b/local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64_x86_64-simulator/JL_OTALib.framework/_CodeSignature/CodeResources @@ -30,7 +30,7 @@ Info.plist - WfMSuIuMgNEEbLR1JZENJAqvP5Y= + L1sSyhac1P15lar1/muRvYYRCUI= Modules/module.modulemap diff --git a/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/MethodRouter.swift b/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/MethodRouter.swift index 8aa684581..ed3b85aa1 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/MethodRouter.swift +++ b/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/MethodRouter.swift @@ -64,7 +64,9 @@ public final class MethodRouter { bleAddress: address, edrAddress: args["edrAddr"] as? String, deviceType: (args["deviceType"] as? NSNumber)?.intValue ?? -1, - connectWay: (args["connectWay"] as? NSNumber)?.intValue ?? 0 + connectWay: (args["connectWay"] as? NSNumber)?.intValue ?? 0, + // Dart 侧明确传 dualConnect=true 才做 BR/EDR 桥接升级,默认纯 BLE。 + dualConnect: (args["dualConnect"] as? Bool) ?? false ) { ok, errMsg in if ok { result(true) } else { result(FlutterError(code: "CONNECT_FAILED", message: errMsg ?? "connect failed", details: nil)) } @@ -181,12 +183,17 @@ public final class MethodRouter { let ch = (args["channels"] as? NSNumber)?.intValue ?? 1 let bits = (args["bitsPerSample"] as? NSNumber)?.intValue ?? 16 let isFinal = args["final"] as? Bool ?? false + // 诊断:只在段尾或路由失败时打。 let ok = server.translationFeature.feedTranslatedAudio( streamId: streamId, pcm: pcm, sampleRate: sr, channels: ch, bitsPerSample: bits, isFinal: isFinal ) + if isFinal || !ok { + NSLog("[MethodRouter] feedTranslatedAudio streamId=%@ pcm=%dB isFinal=%@ routed=%@", + streamId, pcm.count, isFinal ? "true" : "false", ok ? "true" : "false") + } result(ok) case "feedTranslationResult": diff --git a/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/audio/OpusStreamCodec.swift b/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/audio/OpusStreamCodec.swift index 8fb19c560..e64f4846d 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/audio/OpusStreamCodec.swift +++ b/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/audio/OpusStreamCodec.swift @@ -117,3 +117,84 @@ private final class OpusEncoderProxy: NSObject, JLOpusEncoderDelegate { if let data = data, !data.isEmpty { onOpus(data) } } } + +/// 文件级整段 OPUS 编码 —— 与 Android `JieliAITranslationBridge.encodeAndDeliverAsync` 对齐。 +/// +/// 调用 `JLOpusEncoder.opusEncodeFile:output:result:` 把一整段 PCM 转成一整段带完整 head 的 OPUS 流。 +/// 完全不走 delegate 回调(`opusEncoder:Data:error:`),仅仅从输出文件读出成果。 +/// +/// 为什么不用 `opusEncodeData:` 流式接口: +/// - 流式接口 per-frame 给不同的 partial OPUS,每次不一定带 head,拼出来的东西 SDK +/// 切包销资性无法按 “一整段 utterance” 方式写回耳机。 +/// - 文件接口内部完整初始化 OggOpus(hasDataHeader=true)+ 批量写入,与 demo `MachineTranslation.tryToTTS` +/// 给 SDK 的格式一致。 +public enum OpusSegmentEncoder { + /// 对 `pcm` 整段文件级编码,结果在主线程以外的 SDK 内部线程回调。 + /// completion 传回`(opusData, error)`:两者至少一个非 nil;成功时 opusData 非空。 + public static func encode( + pcm: Data, + sampleRate: Int = 16000, + channels: Int = 1, + hasDataHeader: Bool = true, + completion: @escaping (Data?, Error?) -> Void + ) { + guard !pcm.isEmpty else { + completion(nil, NSError(domain: "OpusSegmentEncoder", code: -1, + userInfo: [NSLocalizedDescriptionKey: "empty pcm input"])) + return + } + + let tmpDir = NSTemporaryDirectory() as NSString + let seq = UUID().uuidString + let pcmPath = tmpDir.appendingPathComponent("tts_\(seq).pcm") + let opusPath = tmpDir.appendingPathComponent("tts_\(seq).opus") + + do { + try pcm.write(to: URL(fileURLWithPath: pcmPath)) + } catch { + completion(nil, error) + return + } + + // 对齐 Android:Android 侧 `OpusManager().encodeFile(...)` 不显式设置任何 + // 字段,直接使用 `OpusOption` 的默认配置(带 head)。 + // iOS 这边对应的是 `defaultConfig`(完整默认,带 head),而不是 + // `defaultJL`(注释说明是「杰理的无头配置」,字段默认值和带头模式 + // 不同,在其上手动叠 hasDataHeader=YES + bitRate=16000 + bandwidth=WIDEBAND + // 干预的字段越多、与 Android 缺省行为的偏移就越大,导致耳机 + // 解码后声音扰曲错乱。直接用 defaultConfig 不动任何字段是最安全的对齐方式。 + let cfg = JLOpusEncodeConfig.defaultJL() + cfg.sampleRate = Int32(sampleRate) + cfg.channels = Int32(channels) + + // delegate 必须非 nil(且 weak,需要 stored 保活)。文件接口不会触发 data 回调, + // 只是为满足初始化签名。 + let dummy = _SilentOpusEncoderDelegate() + let encoder = JLOpusEncoder(format: cfg, delegate: dummy) + + encoder.opusEncodeFile(pcmPath, output: opusPath) { outPath, err in + // 保活 dummy,避免被 ARC 提前释放导致 SDK 的 weak delegate 空指。 + _ = dummy + defer { + encoder.opusOnRelease() + try? FileManager.default.removeItem(atPath: pcmPath) + try? FileManager.default.removeItem(atPath: opusPath) + } + if let err = err { + completion(nil, err); return + } + guard let data = try? Data(contentsOf: URL(fileURLWithPath: opusPath)), !data.isEmpty else { + completion(nil, NSError(domain: "OpusSegmentEncoder", code: -2, + userInfo: [NSLocalizedDescriptionKey: "empty opus output"])) + return + } + completion(data, nil) + } + } +} + +private final class _SilentOpusEncoderDelegate: NSObject, JLOpusEncoderDelegate { + func opusEncoder(_ encoder: JLOpusEncoder, data: Data?, error: Error?) { + // 文件级接口不走此回调,留空。 + } +} diff --git a/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/core/TranslationSession.swift b/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/core/TranslationSession.swift index f063f0317..7eaf3842a 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/core/TranslationSession.swift +++ b/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/core/TranslationSession.swift @@ -7,7 +7,7 @@ import JL_BLEKit /// 同一时刻只能服务一个业务场景: /// - `TranslationFeature.<某个 mode handler>`(翻译流) /// - `AssistantBridge`(AI 助理:MODE_RECORD + 设备录音) -/// - `DeviceRecordFeature`(设备录音:MODE=6 + stereo + 设备录音) +/// - `DeviceRecordFeature`(设备录音:MODE=7 + stereo + 设备录音) /// /// 业务方 enter 时把自己注册为 `currentOwner`,所有 SDK 回调(onReceiveAudioData / /// onModeChange / onError / onSendAudioQueueOver / isOnCalling)都会路由到它;exit 时 diff --git a/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/ConnectFeature.swift b/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/ConnectFeature.swift index 2d6d4acf7..f9e6f79fd 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/ConnectFeature.swift +++ b/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/ConnectFeature.swift @@ -1,3 +1,4 @@ +import CoreBluetooth import Foundation import JL_BLEKit @@ -9,6 +10,23 @@ import JL_BLEKit /// - 配对成功后**主动调一次 `cmdTargetFeatureResult`**,以拿到电量 / 版本 / vid-pid 等 /// 基础信息——这一步与 Android `RCSPController.init` 后由 SDK 自动拉取的行为对齐。 /// 完成后向 Dart 推 `rcspInit`,让上层确定 RCSP 已 ready。 +/// +/// # 一键双连(LE + BR/EDR 桥接) +/// Android 端默认走 SPP(经典蓝牙)拉音频通道;iOS 只能以 BLE 方式建连,但可以借助 +/// `CBConnectPeripheralOptionEnableTransportBridgingKey`(WWDC19 引入,iOS 13+) +/// 提示系统"LE proximity 触发 BR/EDR connection"——要求设备支持 CTKD。 +/// +/// ## 实测坑(2026-05 验证) +/// **不要**在 `bleMultiple.connectEntity` 之前抢跑 `connect(options: bridging)`—— +/// iOS 对同一 peripheral 的 connect 请求以**第一次的 options 为准** dedupe,后续 SDK +/// 的无 options connect 会被忽略;如果设备不支持 CTKD 或未配对,我们这条带 bridging +/// 的请求会卡死在等 CTKD,表现为 `peripheralState=0 → connectionState=2` 之后再也不 +/// 前进,BLE 通路本身直接挂掉。 +/// +/// 正确做法:让 SDK 先正常把 BLE 建立好(`.paired/.connectRepeat`),**之后**再发 +/// bridging hint——此时 iOS 会尝试做 CTKD → BR/EDR 升级但不影响已建立的 BLE 通路, +/// 设备不支持 CTKD 也只是升级失败,BLE 可用。并给一个 `dualConnect` 开关,默认关, +/// 由 Dart 侧在设备明确支持 CTKD 时打开。 public final class ConnectFeature { private weak var server: JieliHomeServer? @@ -20,6 +38,7 @@ public final class ConnectFeature { edrAddress: String?, deviceType: Int, connectWay: Int, + dualConnect: Bool = false, completion: @escaping (_ ok: Bool, _ errMsg: String?) -> Void ) { guard let server = server else { completion(false, "server uninitialized"); return } @@ -38,10 +57,25 @@ public final class ConnectFeature { "state": 2, // CONNECTION_CONNECTING ]) + // ⚠️ 不在这里发 bridging hint——实测会把 BLE connect 卡死(见类注释)。 + // 只让 SDK 跑纯 BLE 正常流程,bridging 在 .paired 之后再补。 + NSLog("[JieliConnect] connect uuid=%@ edr=%@ deviceType=%ld connectWay=%ld dualConnect=%@", + bleAddress, edrAddress ?? "nil", deviceType, connectWay, + dualConnect ? "YES" : "NO") + bleMultiple.connectEntity(target) { [weak self] status in guard let self = self else { return } switch status { case .paired, .connectRepeat: + // BLE 已建立。dualConnect=true 时尝试一次 BR/EDR 桥接升级—— + // 对不支持 CTKD 的设备不会影响已建立的 BLE 通路。 + if dualConnect { + ConnectFeature.requestTransportBridging( + centralManager: bleMultiple.getCenterManaer(), + peripheral: target.mPeripheral, + uuid: bleAddress + ) + } // 1) 触发一次 RCSP TargetFeature 拉取,等价于 Android 端 SDK 内部 // 在 RCSPController.init 成功后自动做的事。 target.mCmdManager.cmdTargetFeatureResult { [weak self] state, _, _ in @@ -115,4 +149,34 @@ public final class ConnectFeature { "name": entity.mPeripheral.name ?? "", ] } + + // MARK: - Transport Bridging (LE → BR/EDR, WWDC19) + + /// 向系统申请把 LE 连接桥接到 BR/EDR,实现"一键双连"—— + /// 设备支持 CTKD 时,iOS 会在 BLE 建连同时自动拉起经典蓝牙, + /// HFP / A2DP 等 Classic profile 会自动可用。 + /// + /// 失败不抛错、不阻塞:设备不支持 CTKD 时,系统会忽略这个 option, + /// 后续 SDK 的 `connectEntity` 照常走纯 LE 路径。 + private static func requestTransportBridging( + centralManager: CBCentralManager?, + peripheral: CBPeripheral?, + uuid: String + ) { + guard let cm = centralManager, let p = peripheral else { + NSLog("[JieliConnect] bridging skipped: centralManager/peripheral nil uuid=%@", uuid) + return + } + // iOS 13+ 才有 CBConnectPeripheralOptionEnableTransportBridgingKey。 + if #available(iOS 13.0, *) { + let options: [String: Any] = [ + CBConnectPeripheralOptionEnableTransportBridgingKey: true, + ] + NSLog("[JieliConnect] request BR/EDR bridging uuid=%@ peripheralState=%ld", + uuid, p.state.rawValue) + cm.connect(p, options: options) + } else { + NSLog("[JieliConnect] bridging unavailable on iOS <13 uuid=%@", uuid) + } + } } diff --git a/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/DeviceRecordFeature.swift b/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/DeviceRecordFeature.swift index b3422020a..1b4455f26 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/DeviceRecordFeature.swift +++ b/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/DeviceRecordFeature.swift @@ -5,13 +5,19 @@ import JL_BLEKit /// 设备录音通路(iOS)—— 与 Android `DeviceRecordFeature + JieliDeviceRecordPort` 对齐。 /// /// # 通路细节(与 Android 一致) -/// - 进入 `MODE_CALL_TRANSLATION_WITH_STEREO(6)` + `recordtype = .byDevice` + `channel = 2` +/// - 进入 `MODE_CALL_RECORD(7)` + `recordtype = .byDevice` + `channel = 2` /// - 耳机一路推 SOURCE_E_SCO_MIX 立体声 OPUS(L=本端 / R=对端) /// - 内部用 `OpusStreamDecoder(channels=2, dataSize=80)` 解码成 16k/16bit/stereo 交织 PCM /// - 通过 `deviceRecordAudio` EventChannel 外抛(streamId = "in.stereo") /// /// # 真机注意 -/// 与 Android 同样:mode=6 依赖真实通话事件(SCO_MIX),无通话时固件可能不上推 → 零帧。 +/// mode=7 是专门的通话录音通路(与 mode=6 通话翻译互斥),耳机固件持续上推 SCO_MIX +/// 双声道交织 OPUS,不再像 mode=6 那样需要真实 SCO 通话才上行。 +/// +/// # 与 mode=6 的历史 +/// 早先此通路下发的是 `MODE_CALL_TRANSLATION_WITH_STEREO(6)`,与 Android 对齐后 +/// 固件侧切换到专用的 `MODE_CALL_RECORD(7)`;`JLTranslateSetModeType` Obj-C 枚举 +/// 只显式到 0x06,所以这里用 KVC 写 `modeType = 7` 下发给设备。 public final class DeviceRecordFeature: NSObject, TranslationSessionOwner { private weak var server: JieliHomeServer? @@ -21,7 +27,7 @@ public final class DeviceRecordFeature: NSObject, TranslationSessionOwner { private var deviceAddress: String? private var sampleRate = 16000 - public var sessionModeId: Int { TranslationModeIds.MODE_CALL_TRANSLATION_WITH_STEREO } + public var sessionModeId: Int { TranslationModeIds.MODE_CALL_RECORD } init(server: JieliHomeServer) { self.server = server @@ -61,9 +67,15 @@ public final class DeviceRecordFeature: NSObject, TranslationSessionOwner { return } tm.recordtype = .byDevice - let mode = BaseTranslationModeHandler.buildMode( - modeType: .callTranslateStereo, audioType: .OPUS, channel: 2, sampleRate: sr - ) + // `JLTranslateSetModeType` 枚举只到 0x06,MODE_CALL_RECORD(7) 不是合法 enum case。 + // 通过 KVC 写 modeType,绕开 Swift 枚举校验,底层 Obj-C 仅存 NSUInteger, + // 会原值透传给固件(与 CallRecordModeHandler 完全一致)。 + let mode = JLTranslateSetMode() + mode.dataType = .OPUS + mode.channel = 2 + mode.sampleRate = sr + mode.setValue(NSNumber(value: 7), forKey: "modeType") + NSLog("[DeviceRecord] trStartTranslate mode=MODE_CALL_RECORD(0x07) dataType=OPUS ch=2 sr=\(sr) recordType=byDevice") tm.trStartTranslate(mode) { [weak self] status, err in guard let self = self else { return } if status == .success || status == .sameMode { @@ -102,7 +114,7 @@ public final class DeviceRecordFeature: NSObject, TranslationSessionOwner { public func onReceiveAudio(_ audio: JLTranslateAudio) { guard isRecording else { return } - // mode=6 时 SDK 把上行帧 source 标成 .typeESCOMax (SOURCE_E_SCO_MIX);其他 source 忽略 + // mode=7 与 mode=6 一致:SDK 把上行帧 source 标成 .typeESCOMax (SOURCE_E_SCO_MIX);其他 source 忽略 guard audio.sourceType == .typeESCOMax else { return } switch audio.audioType { case .OPUS: decoder?.feed(audio.data) @@ -114,7 +126,7 @@ public final class DeviceRecordFeature: NSObject, TranslationSessionOwner { public func onModeChange(_ mode: JLTranslateSetMode) { if mode.modeType == .idle && isRecording { emitError("device.record.mode_exited", code: -1, - message: "headset exited MODE=6 → MODE_IDLE") + message: "headset exited MODE=7 → MODE_IDLE") stop() } } diff --git a/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/TranslationFeature.swift b/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/TranslationFeature.swift index fd9aedf0e..587f969e7 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/TranslationFeature.swift +++ b/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/TranslationFeature.swift @@ -1,12 +1,12 @@ import Foundation import JL_BLEKit -/// 翻译特性入口(iOS)—— 6-mode 分发器,对齐 Android `TranslationFeature` 的 method/event 表面。 +/// 翻译特性入口(iOS)—— 7-mode 分发器,对齐 Android `TranslationFeature` 的 method/event 表面。 /// /// # 与 Android 行为差异(按 iOS demo 实测能力定) /// - **不做 mode=3 → mode=6 自动升级**:iOS demo 由用户手动二选一(callTranslate / callTranslateStereo), /// 我们也沿用这个语义。Dart 端按需直接选 modeId。 -/// - 6 个 mode handler 共享同一个 [TranslationSession](同一台设备一个 `JLTranslationManager`), +/// - 7 个 mode handler 共享同一个 [TranslationSession](同一台设备一个 `JLTranslationManager`), /// 切换 mode 时先 stop 旧的、再 start 新的。 public final class TranslationFeature { @@ -24,6 +24,7 @@ public final class TranslationFeature { TranslationModeIds.MODE_CALL_TRANSLATION_WITH_STEREO: StereoCallTranslationModeHandler(server: server), TranslationModeIds.MODE_AUDIO_TRANSLATION: AudioTranslationModeHandler(server: server), TranslationModeIds.MODE_FACE_TO_FACE_TRANSLATION: FaceToFaceTranslationModeHandler(server: server), + TranslationModeIds.MODE_CALL_RECORD: CallRecordModeHandler(server: server), ] } @@ -51,8 +52,16 @@ public final class TranslationFeature { sampleRate: Int, channels: Int, bitsPerSample: Int, isFinal: Bool ) -> Bool { - guard let h = current, h.isWorking else { return false } - if !h.outputStreams.contains(streamId) { return false } + guard let h = current, h.isWorking else { + NSLog("[TranslationFeature] feedTranslatedAudio DROPPED: no current/working handler streamId=%@ pcm=%dB isFinal=%@", + streamId, pcm.count, isFinal ? "true" : "false") + return false + } + if !h.outputStreams.contains(streamId) { + NSLog("[TranslationFeature] feedTranslatedAudio DROPPED: streamId=%@ not in handler.outputStreams=%@ (modeId=%d)", + streamId, h.outputStreams.description, h.modeId) + return false + } return h.onTranslatedAudio( streamId: streamId, pcm: pcm, format: AudioFormat(sampleRate: sampleRate, channels: channels, bitsPerSample: bitsPerSample), diff --git a/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/translation/TranslationConstants.swift b/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/translation/TranslationConstants.swift index 83e5c59ea..89eb95c70 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/translation/TranslationConstants.swift +++ b/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/translation/TranslationConstants.swift @@ -6,6 +6,7 @@ public enum TranslationStreams { public static let IN_UPLINK = "in.uplink" // 通话上行:本机用户 public static let IN_DOWNLINK = "in.downlink" // 通话下行:对端 public static let IN_AUDIO_FILE = "in.audioFile" + public static let IN_STEREO = "in.stereo" // 双声道交织 PCM:L=本端 / R=对端 public static let OUT_SPEAKER = "out.speaker" public static let OUT_UPLINK = "out.uplink" @@ -14,6 +15,10 @@ public enum TranslationStreams { } /// 与 SDK `JLTranslateSetModeType` 完全一致;和 Dart `TranslationModeIds` 对齐。 +/// +/// 注:SDK 的 `JLTranslateSetModeType` 枚举只显式定义到 0x06,`MODE_CALL_RECORD = 7` +/// 对齐 Android `TranslationMode.MODE_CALL_RECORD`(SDK 内部值 7)—— 耳机固件支持, +/// 通过 `JLTranslateSetModeType(rawValue: 7)` 直接下发到设备。 public enum TranslationModeIds { public static let MODE_IDLE = 0 public static let MODE_RECORD = 1 @@ -22,6 +27,7 @@ public enum TranslationModeIds { public static let MODE_AUDIO_TRANSLATION = 4 public static let MODE_FACE_TO_FACE_TRANSLATION = 5 public static let MODE_CALL_TRANSLATION_WITH_STEREO = 6 + public static let MODE_CALL_RECORD = 7 } /// 一帧 PCM 音频规格 diff --git a/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/translation/mode/CallRecordModeHandler.swift b/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/translation/mode/CallRecordModeHandler.swift new file mode 100644 index 000000000..25eed4c5b --- /dev/null +++ b/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/translation/mode/CallRecordModeHandler.swift @@ -0,0 +1,191 @@ +import Foundation +import JL_BLEKit + +/// MODE_CALL_RECORD(mode=7)—— 通话录音(设备采集,无 TTS 回写)。 +/// +/// 与 Android `JieliDeviceRecordPort` 对齐: +/// - 下发 `mode=7 / audioType=OPUS / channel=2 / sampleRate=16k / recordtype=byDevice` +/// - 耳机持续上推 `SOURCE_E_SCO_MIX` 双声道交织 OPUS(L=本端 / R=对端) +/// - `OpusStreamDecoder(channels=2, dataSize=80)` 解码成 16k/16bit/stereo 交织 PCM +/// - 以单路 `in.stereo` 向 Dart 抛 PCM;**无 output 流**,不接 TTS 回写 +/// +/// 与 [StereoCallTranslationModeHandler](mode=6)的区别: +/// - mode=6 是立体声"通话翻译":耳机把 mix 上推、APP 可把译文回写给耳机 +/// - mode=7 是专门的"通话录音":只采集,不回写,固件通路与通话翻译互斥 +/// +/// 注:SDK 的 `JLTranslateSetModeType` 枚举没有 0x07,但它是 `NS_ENUM(NSUInteger, …)`, +/// 可以用 `JLTranslateSetModeType(rawValue: 7)` 构造未命名 case 下发到设备。 +public final class CallRecordModeHandler: BaseTranslationModeHandler { + + private var decoder: OpusStreamDecoder? + private var sampleRate = 16000 + private var audioType: JL_SpeakDataType = .OPUS + private var rxFrameCount: Int64 = 0 + private var rxBytes: Int64 = 0 + private var heartbeatTimer: DispatchSourceTimer? + private let heartbeatQueue = DispatchQueue(label: "callrecord.heartbeat", qos: .utility) + + public init(server: JieliHomeServer) { + super.init( + server: server, + modeId: TranslationModeIds.MODE_CALL_RECORD, + inputs: [TranslationStreams.IN_STEREO], + outputs: [] + ) + } + + public override func start(args: [String: Any], completion: @escaping (Bool, String?) -> Void) { + print("[CallRecord] start ENTRY args=\(args)") + guard let server = server else { + completion(false, "server uninitialized"); return + } + if working { + print("[CallRecord] start already working, returning ok") + completion(true, nil); return + } + + let address = args["address"] as? String + let session: TranslationSession? = { + if let addr = address { return server.translationSession(for: addr) } + return server.currentTranslationSession() + }() + guard let s = session else { + emitError(-200, "no connected device; pass args.address or connect first") + completion(false, "no connected device"); return + } + + let sr = (args["sampleRate"] as? NSNumber)?.intValue ?? 16000 + let aType = Self.parseAudioType(args["audioType"]) ?? .OPUS + // 通话录音固件通路强制由耳机侧采集;`byPhone` 不适用,这里默认并只允许 byDevice。 + let recordType = Self.parseRecordType(args["strategy"], default: .byDevice) + print("[CallRecord] start params sr=\(sr) audioType=\(aType.rawValue) recordType=\(recordType.rawValue)") + + sampleRate = sr + audioType = aType + self.session = s + + // 立体声 OPUS 解码:单帧 80 B(16k/16bit/stereo/20ms)。 + if aType == .OPUS { + decoder = OpusStreamDecoder(channels: 2, dataSize: 80, sampleRate: sr) { [weak self] stereoPcm in + self?.emitStereo(stereoPcm) + } onError: { [weak self] code, msg in + self?.emitError(code, "opus stereo decode: \(msg ?? "")") + } + } + + s.acquire(owner: self) { [weak self] tm, errMsg in + guard let self = self else { return } + guard let tm = tm else { + print("[CallRecord] acquire FAIL err=\(errMsg ?? "nil")") + self.emitError(-201, errMsg) + completion(false, errMsg); return + } + print("[CallRecord] acquire OK isSupportTranslate=\(tm.trIsSupportTranslate()) " + + "isPlayWithA2dp=\(tm.trIsPlayWithA2dp()) isWorking=\(tm.trIsWorking()) isCalling=\(tm.isCalling)") + + tm.recordtype = recordType + // JLTranslateSetModeType 枚举只显式定义到 0x06,0x07 不是合法 enum case: + // - `JLTranslateSetModeType(rawValue: 7)` 在 NS_ENUM 严格模式下会返回 nil + // - 这里直接用 KVC 写 modeType 属性,绕过 Swift 枚举校验,底层 Obj-C + // 仅存个 NSUInteger,enterMode 单测是按原值透传给设备的 + let mode = JLTranslateSetMode() + mode.dataType = aType + mode.channel = 2 + mode.sampleRate = sr + mode.setValue(NSNumber(value: 7), forKey: "modeType") + self.emitLog("CallRecord start audioType=\(aType.rawValue) sr=\(sr) recordType=\(recordType.rawValue) ch=2") + print("[CallRecord] calling trStartTranslate mode=MODE_CALL_RECORD(0x07) dataType=\(aType.rawValue) ch=2 sr=\(sr)") + tm.trStartTranslate(mode) { [weak self] status, err in + guard let self = self else { return } + print("[CallRecord] trStartTranslate completion status=\(status.rawValue) err=\(err?.localizedDescription ?? "nil")") + if status == .success || status == .sameMode { + self.working = true + NSLog("[CallRecord] ✅ 通话录音启动成功 (status=\(status.rawValue) mode=7 sr=\(sr) recordType=\(recordType.rawValue)) — 等待耳机 SCO_MIX 上行...") + self.startHeartbeat() + completion(true, nil) + } else { + NSLog("[CallRecord] ❌ 通话录音启动失败 status=\(status.rawValue) err=\(err?.localizedDescription ?? "nil")") + self.emitError(Int(status.rawValue), err?.localizedDescription ?? "trStartTranslate failed") + self.stop() + completion(false, "tm status=\(status.rawValue)") + } + } + } + } + + public override func stop() { + stopHeartbeat() + guard let s = session, working else { working = false; return } + working = false + s.release(owner: self) + decoder?.release(); decoder = nil + NSLog("[CallRecord] 🛑 CallRecord stop (totalRxFrames=\(rxFrameCount) bytes=\(rxBytes))") + emitLog("CallRecord stop") + } + + // MARK: - Audio upstream (SDK → APP) + + public override func onReceiveAudio(_ audio: JLTranslateAudio) { + rxFrameCount &+= 1 + if rxFrameCount <= 5 || rxFrameCount % 100 == 1 { + let msg = "[CallRecord/onRx] #\(rxFrameCount) working=\(working) " + + "source=\(audio.sourceType.rawValue) audioType=\(audio.audioType.rawValue) " + + "len=\(audio.data.count) count=\(audio.count)" + NSLog("%@", msg) + emitLog(msg) + } + guard working else { return } + // mode=7 与 mode=6 一致:SDK 把上行帧 source 标成 .typeESCOMax (SOURCE_E_SCO_MIX) + guard audio.sourceType == .typeESCOMax else { + if rxFrameCount <= 5 || rxFrameCount % 100 == 1 { + emitLog("[CallRecord/onRx] drop unhandled sourceType=\(audio.sourceType.rawValue)") + } + return + } + rxBytes &+= Int64(audio.data.count) + switch audio.audioType { + case .OPUS: decoder?.feed(audio.data) + case .PCM: emitStereo(audio.data) + default: + // 其它编码暂不支持(通话录音固件只会推 OPUS/PCM),丢弃并打日志 + if rxFrameCount <= 5 || rxFrameCount % 100 == 1 { + emitLog("[CallRecord/onRx] drop unsupported audioType=\(audio.audioType.rawValue)") + } + } + } + + // MARK: - Heartbeat + + private func startHeartbeat() { + stopHeartbeat() + rxFrameCount = 0 + rxBytes = 0 + let t = DispatchSource.makeTimerSource(queue: heartbeatQueue) + t.schedule(deadline: .now() + 2, repeating: 2.0) + t.setEventHandler { [weak self] in + guard let self = self, self.working else { return } + if self.rxFrameCount == 0 { + NSLog("[CallRecord/Heartbeat] ⚠️ 通话录音运行中,但耳机上行 0 帧 — 请确认设备是否处于真实通话状态(SCO_MIX 仅在通话期间上推)。") + } else { + NSLog("[CallRecord/Heartbeat] ✓ rx=\(self.rxFrameCount) 帧 bytes=\(self.rxBytes)") + } + } + heartbeatTimer = t + t.resume() + } + + private func stopHeartbeat() { + heartbeatTimer?.cancel() + heartbeatTimer = nil + } + + // MARK: - Helpers + + private func emitStereo(_ pcm: Data) { + pushFrame( + streamId: TranslationStreams.IN_STEREO, + pcm: pcm, + format: AudioFormat(sampleRate: sampleRate, channels: 2, bitsPerSample: 16) + ) + } +} diff --git a/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/translation/mode/CallTranslationModeHandler.swift b/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/translation/mode/CallTranslationModeHandler.swift index 3d7fffa79..93d4f0825 100644 --- a/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/translation/mode/CallTranslationModeHandler.swift +++ b/local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/translation/mode/CallTranslationModeHandler.swift @@ -1,21 +1,28 @@ import Foundation import JL_BLEKit -/// MODE_CALL_TRANSLATION(mode=3)—— 通话翻译,eSCO 上下行各一路单声道。 +/// MODE_CALL_TRANSLATION(iOS 固件走 mode=6 立体声路径)—— 通话翻译。 /// -/// 与 Android `CallTranslationModeHandler` 对齐: -/// - 输入:`in.uplink`(本机用户麦克风) + `in.downlink`(对端通话音) -/// - 输出:`out.uplink`(译文给对端) + `out.downlink`(译文给本机) -/// - 默认 OPUS 编码、`byDevice` 录音策略(设备始终录音、APP 不开手机麦) +/// 为什么 iOS 端硬绑到 mode=6: +/// - 在当前版本的杰理耳机固件上,mode=3(单声道通话翻译)不会把 eSCO 上/下行 +/// 帧通过 cmd=52 推到 SDK;实测 `onReceiveAudioData` 永远 0 帧。 +/// - mode=6(MODE_CALL_TRANSLATION_WITH_STEREO)走的是同一颗芯片内部 +/// SOURCE_E_SCO_MIX 通路:耳机直接把 uplink(L) + downlink(R) 混成立体声 OPUS +/// 推上来,再由 APP 端软件分声道。Android 在支持此能力时也会自动升级到 6。 /// -/// 上行:耳机推 OPUS → `OpusStreamDecoder` 解码 → 按 sourceType 分流 (`in.uplink` / `in.downlink`) -/// 下行:Dart 喂 PCM → `OpusStreamEncoder` 编码 → `JLTranslationManager.trWriteAudio` 回耳机 +/// 约定: +/// - `modeId` 对外仍然是 MODE_CALL_TRANSLATION,保持 Dart 层逻辑不变。 +/// - 下发给固件的 mode 固定是 `callTranslateStereo(0x06)`, channel=2。 +/// - 默认 OPUS、`byDevice` 录音策略。 +/// +/// 上行:耳机推 **立体声** OPUS → 2 声道解码 → `PcmKit.splitStereo16` 拆 L/R +/// → L 进 `in.uplink`、R 进 `in.downlink` +/// 下行:Dart 喂 PCM → 单声道 OPUS 编码 → 按 source 写回耳机(`typeESCOUp`/`typeESCODown`) public final class CallTranslationModeHandler: BaseTranslationModeHandler { private var decoder: OpusStreamDecoder? private var encoderUplink: OpusStreamEncoder? private var encoderDownlink: OpusStreamEncoder? - private var pendingAudioSource: JLTranslateAudioSourceType = .typeESCOUp private var sampleRate = 16000 private var audioType: JL_SpeakDataType = .OPUS private var rxFrameCount: Int64 = 0 @@ -24,6 +31,19 @@ public final class CallTranslationModeHandler: BaseTranslationModeHandler { private var heartbeatTimer: DispatchSourceTimer? private let heartbeatQueue = DispatchQueue(label: "calltrans.heartbeat", qos: .utility) + // TTS 下行:per-leg PCM / OPUS 累积缓冲。isFinal=true 时一次性编码 + 下发。 + // 这条“整段开闸”语义对应 Android `JieliAITranslationBridge.feedTtsPcm`; + // 固件/SDK 期望的是“一次 trWriteAudioV2 = 一段完整 utterance OPUS(带 head)”。 + private let ttsBufferLock = NSLock() + private var pcmBufferUplink = Data() + private var pcmBufferDownlink = Data() + private var opusBufferUplink = Data() + private var opusBufferDownlink = Data() + // 兜底:上层一直不发 isFinal 时避免内存爆,和 Android `PCM_BUFFER_HARD_LIMIT_BYTES` 对齐。 + private let pcmBufferHardLimitBytes = 2 * 1024 * 1024 + // 诊断:TTS 入口调用计数,用于节流打印入口日志。 + private var ttsInCount: Int64 = 0 + public init(server: JieliHomeServer) { super.init( server: server, @@ -65,23 +85,28 @@ public final class CallTranslationModeHandler: BaseTranslationModeHandler { audioType = aType self.session = s - // 解码 / 编码器(仅 OPUS 模式需要) + // 编/解码器(仅 OPUS 模式需要) if aType == .OPUS { - decoder = OpusStreamDecoder(channels: 1, dataSize: 40, sampleRate: sr) { [weak self] pcm in + // 立体声 OPUS。channels=2、dataSize 参考 [StereoCallTranslationModeHandler] 给 80。 + decoder = OpusStreamDecoder(channels: 2, dataSize: 80, sampleRate: sr) { [weak self] stereoPcm in guard let self = self else { return } - let stream = self.pendingAudioSource == .typeESCODown ? - TranslationStreams.IN_DOWNLINK : TranslationStreams.IN_UPLINK - self.pushFrame(streamId: stream, pcm: pcm, format: AudioFormat(sampleRate: sr, channels: 1, bitsPerSample: 16)) + // stereo PCM (interleaved L,R) → 拆成两路单声道 + let (l, r) = PcmKit.splitStereo16(stereoPcm) + let fmt = AudioFormat(sampleRate: sr, channels: 1, bitsPerSample: 16) + self.rxUplinkBytes &+= Int64(l.count) + self.rxDownlinkBytes &+= Int64(r.count) + self.pushFrame(streamId: TranslationStreams.IN_UPLINK, pcm: l, format: fmt) + self.pushFrame(streamId: TranslationStreams.IN_DOWNLINK, pcm: r, format: fmt) } onError: { [weak self] code, msg in - self?.emitError(code, "opus decode: \(msg ?? "")") + self?.emitError(code, "opus stereo decode: \(msg ?? "")") } - encoderUplink = OpusStreamEncoder(channels: 1, sampleRate: sr) { [weak self] opus in - self?.writeBackAudio(opus, source: .typeESCOUp) + encoderUplink = OpusStreamEncoder(channels: 1, sampleRate: sr, hasDataHeader: true) { [weak self] opus in + self?.appendOpus(opus, to: TranslationStreams.OUT_UPLINK) } onError: { [weak self] code, msg in self?.emitError(code, "opus encode uplink: \(msg ?? "")") } - encoderDownlink = OpusStreamEncoder(channels: 1, sampleRate: sr) { [weak self] opus in - self?.writeBackAudio(opus, source: .typeESCODown) + encoderDownlink = OpusStreamEncoder(channels: 1, sampleRate: sr, hasDataHeader: true) { [weak self] opus in + self?.appendOpus(opus, to: TranslationStreams.OUT_DOWNLINK) } onError: { [weak self] code, msg in self?.emitError(code, "opus encode downlink: \(msg ?? "")") } @@ -98,15 +123,23 @@ public final class CallTranslationModeHandler: BaseTranslationModeHandler { "isSupportTranslate=\(tm.trIsSupportTranslate()) isPlayWithA2dp=\(tm.trIsPlayWithA2dp()) " + "isWorking=\(tm.trIsWorking()) isCalling=\(tm.isCalling)") tm.recordtype = recordType - let mode = Self.buildMode(modeType: .callTranslate, audioType: aType, channel: 1, sampleRate: sr) - self.emitLog("CallTranslation start audioType=\(aType.rawValue) sr=\(sr) recordType=\(recordType.rawValue)") - print("[CallTrans] calling trStartTranslateMode mode=callTranslate(0x03) dataType=\(aType.rawValue) ch=1 sr=\(sr)") + // iOS 端硬绑 mode=6:在当前固件上 mode=3 不推帧,只有 mode=6 会把 + // SOURCE_E_SCO_MIX 立体声帧推上来。维持 isCalling / trSendIsRelay 这两 + // 个“开闸”也一并保留。 + tm.isCalling = true + let mode = Self.buildMode(modeType: .callTranslateStereo, audioType: aType, channel: 2, sampleRate: sr) + self.emitLog("CallTranslation start (mode=6 stereo) audioType=\(aType.rawValue) sr=\(sr) recordType=\(recordType.rawValue)") + print("[CallTrans] calling trStartTranslateMode mode=callTranslateStereo(0x06) dataType=\(aType.rawValue) ch=2 sr=\(sr)") tm.trStartTranslate(mode) { [weak self] status, err in guard let self = self else { return } print("[CallTrans] trStartTranslateMode completion status=\(status.rawValue) err=\(err?.localizedDescription ?? "nil")") if status == .success || status == .sameMode { self.working = true - NSLog("[CallTrans] ✅ 通话翻译启动成功 (status=\(status.rawValue) mode=3 sr=\(sr) audioType=\(aType.rawValue) recordType=\(recordType.rawValue)) — 等待耳机上行音频...") + // 对齐 Android `IAITranslationApi.startTranslating -> callback.onStart()` 语义: + // SDK 需要 APP 显式告知「已准备好接收上行音频」才会开始把固件的 + // cmd=52 帧通过 onReceiveAudioData 推上来。 + tm.trSendIsRelay() + NSLog("[CallTrans] ✅ 通话翻译启动成功 (status=\(status.rawValue) mode=6 stereo sr=\(sr) audioType=\(aType.rawValue) recordType=\(recordType.rawValue)) — 已发 trSendIsRelay,等待耳机立体声上行音频...") self.startHeartbeat() completion(true, nil) } else { @@ -127,6 +160,12 @@ public final class CallTranslationModeHandler: BaseTranslationModeHandler { decoder?.release(); decoder = nil encoderUplink?.release(); encoderUplink = nil encoderDownlink?.release(); encoderDownlink = nil + // 清空 TTS 累积缓冲,避免下次启动残留。 + ttsBufferLock.lock() + pcmBufferUplink = Data(); pcmBufferDownlink = Data() + opusBufferUplink = Data(); opusBufferDownlink = Data() + ttsBufferLock.unlock() + ttsInCount = 0 NSLog("[CallTrans] 🛑 CallTranslation stop (totalRxFrames=\(rxFrameCount) uplinkBytes=\(rxUplinkBytes) downlinkBytes=\(rxDownlinkBytes))") emitLog("CallTranslation stop") } @@ -161,7 +200,7 @@ public final class CallTranslationModeHandler: BaseTranslationModeHandler { public override func onReceiveAudio(_ audio: JLTranslateAudio) { rxFrameCount &+= 1 - // 诊断日志:前 5 帧每帧都打,之后每 100 帧打一次。同时打到 NSLog 和 emitLog。 + // 诊断日志:前 5 帧每帧都打,之后每 100 帧打一次。 if rxFrameCount <= 5 || rxFrameCount % 100 == 1 { let msg = "[CallTrans/onRx] #\(rxFrameCount) working=\(working) " + "source=\(audio.sourceType.rawValue) audioType=\(audio.audioType.rawValue) " + @@ -170,86 +209,199 @@ public final class CallTranslationModeHandler: BaseTranslationModeHandler { emitLog(msg) } guard working else { return } - let stream: String - switch audio.sourceType { - case .typeESCOUp, .typeDeviceMic, .typePhoneMic: - stream = TranslationStreams.IN_UPLINK - rxUplinkBytes &+= Int64(audio.data.count) - case .typeESCODown: - stream = TranslationStreams.IN_DOWNLINK - rxDownlinkBytes &+= Int64(audio.data.count) - default: + // mode=6 下耳机只推 SOURCE_E_SCO_MIX(typeESCOMax)一路立体声帧。其它 + // source 如果真的来了,应当当成异常丢掉,而不是错误地当单声道处理。 + guard audio.sourceType == .typeESCOMax else { if rxFrameCount <= 5 || rxFrameCount % 100 == 1 { - let msg = "[CallTrans/onRx] drop unhandled sourceType=\(audio.sourceType.rawValue)" + let msg = "[CallTrans/onRx] drop unexpected sourceType=\(audio.sourceType.rawValue) (expected typeESCOMax in stereo mode=6)" NSLog("%@", msg) emitLog(msg) } return } - pendingAudioSource = audio.sourceType switch audio.audioType { - case .PCM: - pushFrame(streamId: stream, pcm: audio.data, - format: AudioFormat(sampleRate: sampleRate, channels: 1, bitsPerSample: 16)) case .OPUS: - decoder?.feed(audio.data) + decoder?.feed(audio.data) // 解码后在 decoder 回调里拆 L/R 推 IN_UPLINK / IN_DOWNLINK + case .PCM: + // 直接立体声 PCM(interleaved L,R, 16bit),立即软分声道 + let (l, r) = PcmKit.splitStereo16(audio.data) + let fmt = AudioFormat(sampleRate: sampleRate, channels: 1, bitsPerSample: 16) + rxUplinkBytes &+= Int64(l.count) + rxDownlinkBytes &+= Int64(r.count) + pushFrame(streamId: TranslationStreams.IN_UPLINK, pcm: l, format: fmt) + pushFrame(streamId: TranslationStreams.IN_DOWNLINK, pcm: r, format: fmt) default: - // Speex / JLA_V2 / MSBC 等先原样透传,并标注 encoding - let encoding: String = { - switch audio.audioType { - case .SPEEX: return "speex" - case .JLA_V2: return "jla_v2" - case .MSBC: return "msbc" - default: return "unknown" - } - }() - pushFrame(streamId: stream, pcm: audio.data, - format: AudioFormat(sampleRate: sampleRate, channels: 1, bitsPerSample: 16), - encoding: encoding) + // Speex / JLA_V2 / MSBC 等编码暂不支持立体声软分:记日志丢弃。 + if rxFrameCount <= 5 || rxFrameCount % 100 == 1 { + let msg = "[CallTrans/onRx] drop non-OPUS/PCM audioType=\(audio.audioType.rawValue) in stereo mode" + NSLog("%@", msg) + emitLog(msg) + } } } // MARK: - TTS downstream (APP → SDK) public override func onTranslatedAudio(streamId: String, pcm: Data, format: AudioFormat, isFinal: Bool) -> Bool { + ttsInCount &+= 1 + // 入口日志:首 5 次 + 每 50 次 + isFinal 必打 + working=false 必打。 + if ttsInCount <= 5 || ttsInCount % 50 == 0 || isFinal || !working { + NSLog("[CallTrans/feedTTS-IN] #\(ttsInCount) streamId=\(streamId) pcm=\(pcm.count)B isFinal=\(isFinal) working=\(working) audioType=\(audioType.rawValue)") + } guard working else { return false } switch streamId { case TranslationStreams.OUT_UPLINK: - return feed(pcm: pcm, encoderTo: encoderUplink, source: .typeESCOUp) + return feed(pcm: pcm, encoderTo: encoderUplink, source: .typeESCOUp, leg: streamId, isFinal: isFinal) case TranslationStreams.OUT_DOWNLINK: - return feed(pcm: pcm, encoderTo: encoderDownlink, source: .typeESCODown) + return feed(pcm: pcm, encoderTo: encoderDownlink, source: .typeESCODown, leg: streamId, isFinal: isFinal) default: + NSLog("[CallTrans/feedTTS-IN] unknown streamId=\(streamId), drop") return false } } - private func feed(pcm: Data, encoderTo encoder: OpusStreamEncoder?, source: JLTranslateAudioSourceType) -> Bool { - if audioType == .OPUS { - // 拆 640 字节一帧(16k/16bit/mono/20ms)后送编码器 - let frameBytes = 640 - var ofs = 0 - while ofs < pcm.count { - let len = min(frameBytes, pcm.count - ofs) - encoder?.feed(pcm.subdata(in: ofs..<(ofs + len))) - ofs += len + /// 对齐 Android `JieliAITranslationBridge.feedTtsPcm`: + /// - PCM 模式:每次调用直接作为一段 writeBack。 + /// - OPUS 模式:纯 isFinal 驱动—— 累积 PCM;`isFinal=true` 时用整段 + /// PCM 一次性喛给 encoder,收集所有 `onOpus` 回调拼成整段带 head 的 OPUS,然后 + /// **一次** `trWriteAudioV2` 下发给 SDK/固件。 + /// - 兜底:缓冲超过 `pcmBufferHardLimitBytes` 强制 flush,和 Android 一致。 + private func feed( + pcm: Data, + encoderTo encoder: OpusStreamEncoder?, + source: JLTranslateAudioSourceType, + leg: String, + isFinal: Bool + ) -> Bool { + if audioType != .OPUS { + // PCM 模式:直接单次下发(Android PCM 模式也是这么做) + if !pcm.isEmpty { + writeBackAudio(pcm, source: source, audioType: .PCM) } return true + } + + // OPUS 模式:累积 + isFinal/超限触发整段编码 + var pendingPcm: Data? = nil + var bufferAfter: Int = 0 + ttsBufferLock.lock() + do { + if !pcm.isEmpty { + let currentSize = pcmBufferSize(of: leg) + if currentSize + pcm.count > pcmBufferHardLimitBytes { + NSLog("[CallTrans] feedTTS leg=%@ buffer hit hard limit %d, force flush", leg, pcmBufferHardLimitBytes) + var out = takePcmBuffer(of: leg) + out.append(pcm) + pendingPcm = out + } else { + appendPcm(pcm, to: leg) + } + } + if pendingPcm == nil && isFinal { + let out = takePcmBuffer(of: leg) + pendingPcm = out.isEmpty ? nil : out + } + bufferAfter = pcmBufferSize(of: leg) + } + ttsBufferLock.unlock() + + // 累积阶段节流日志:每次超过 100KB 边界打一条。 + if pendingPcm == nil && !pcm.isEmpty { + let prevK = (bufferAfter - pcm.count) / (100 * 1024) + let nowK = bufferAfter / (100 * 1024) + if nowK > prevK { + NSLog("[CallTrans] feedTTS leg=%@ buffering... pcm=%dB total=%dB (waiting isFinal)", leg, pcm.count, bufferAfter) + } + } + + guard let pcmSegment = pendingPcm else { return true } + + // 文件级整段编码 — 与 Android `encodeAndDeliverAsync` 对齐。 + // JL SDK 的 `opusEncodeFile` 产出带完整 head 的整段 OPUS 流,直接 `trWriteAudioV2` 下发。 + // (流式的 `opusEncodeData:` 实测在当前场景下回调不触发,opus 输出=0) + let sr = self.sampleRate + OpusSegmentEncoder.encode( + pcm: pcmSegment, + sampleRate: sr, + channels: 1, + hasDataHeader: true + ) { [weak self] opus, err in + guard let self = self else { return } + if let err = err { + NSLog("[CallTrans] feedTTS leg=%@ final=%@ pcm=%dB opus=0B encode error: %@", + leg, isFinal ? "true" : "false", pcmSegment.count, err.localizedDescription) + return + } + guard let opusSegment = opus, !opusSegment.isEmpty else { + NSLog("[CallTrans] feedTTS leg=%@ final=%@ pcm=%dB opus=0B (empty after file encode, skip)", + leg, isFinal ? "true" : "false", pcmSegment.count) + return + } + NSLog("[CallTrans] feedTTS leg=%@ final=%@ pcm=%dB opus=%dB \u{2192} trWriteAudio", + leg, isFinal ? "true" : "false", pcmSegment.count, opusSegment.count) + self.writeBackAudio(opusSegment, source: source, audioType: .OPUS) + } + return true + } + + // MARK: - per-leg buffer helpers (调用方需持有 ttsBufferLock) + + private func pcmBufferSize(of leg: String) -> Int { + return leg == TranslationStreams.OUT_UPLINK ? pcmBufferUplink.count : pcmBufferDownlink.count + } + + private func appendPcm(_ data: Data, to leg: String) { + if leg == TranslationStreams.OUT_UPLINK { pcmBufferUplink.append(data) } + else { pcmBufferDownlink.append(data) } + } + + private func takePcmBuffer(of leg: String) -> Data { + if leg == TranslationStreams.OUT_UPLINK { + let out = pcmBufferUplink; pcmBufferUplink = Data(); return out } else { - // 直接 PCM 回写 - writeBackAudio(pcm, source: source, audioType: .PCM) - return true + let out = pcmBufferDownlink; pcmBufferDownlink = Data(); return out } } + private func resetOpusBuffer(of leg: String) { + if leg == TranslationStreams.OUT_UPLINK { opusBufferUplink = Data() } + else { opusBufferDownlink = Data() } + } + + private func takeOpusBuffer(of leg: String) -> Data { + if leg == TranslationStreams.OUT_UPLINK { + let out = opusBufferUplink; opusBufferUplink = Data(); return out + } else { + let out = opusBufferDownlink; opusBufferDownlink = Data(); return out + } + } + + /// encoder onOpus 回调 → 写入 leg 的 opusBuffer。 + fileprivate func appendOpus(_ opus: Data, to leg: String) { + ttsBufferLock.lock() + defer { ttsBufferLock.unlock() } + if leg == TranslationStreams.OUT_UPLINK { opusBufferUplink.append(opus) } + else { opusBufferDownlink.append(opus) } + } + private func writeBackAudio(_ data: Data, source: JLTranslateAudioSourceType, audioType: JL_SpeakDataType? = nil) { - guard let tm = session?.currentManager() else { return } + guard let tm = session?.currentManager() else { + NSLog("[CallTrans] writeBackAudio DROPPED: no current manager (source=%d, len=%d)", source.rawValue, data.count) + return + } + let aType = audioType ?? self.audioType let audio = JLTranslateAudio() audio.sourceType = source - audio.audioType = audioType ?? self.audioType + audio.audioType = aType audio.data = data audio.len = Int32(data.count) audio.count = 1 - tm.trWriteAudioV2(audio, translate: data) + // 关键:OPUS 整段(带 head)必须走 `trWriteAudio`(交互式,SDK 按内部队列按帧节奏推 cmd=52)。 + // `trWriteAudioV2` 头注释明确说明:“无交互式下发,当前默认 20ms/42B jlv2 压缩数据,不支持修改”, + // 整段 OPUS 会被它按 42B 硬切,耳机端解不出来(日志写回成功但无声即此)。 + NSLog("[CallTrans] writeBackAudio \u{2192} trWriteAudio source=%d audioType=%d len=%d", + source.rawValue, aType.rawValue, data.count) + tm.trWrite(audio, translate: data) } } diff --git a/local_plugins/device_jieli/lib/device_jieli.dart b/local_plugins/device_jieli/lib/device_jieli.dart index 4f60b7d76..f35fc54b9 100644 --- a/local_plugins/device_jieli/lib/device_jieli.dart +++ b/local_plugins/device_jieli/lib/device_jieli.dart @@ -156,6 +156,10 @@ class TranslationModeIds { static const int audioTranslation = 4; static const int faceToFaceTranslation = 5; static const int callTranslationWithStereo = 6; + /// MODE_CALL_RECORD:通话录音专用 mode(与 Android `TranslationMode.MODE_CALL_RECORD`=7 对齐)。 + /// - 耳机一路推双声道交织 OPUS,解码后以 `TranslationStreams.inStereo` 单路抛出 + /// - 没有 output 流,不接 TTS 回写 + static const int callRecord = 7; } class TranslationStreams { @@ -163,6 +167,8 @@ class TranslationStreams { static const String inUplink = 'in.uplink'; static const String inDownlink = 'in.downlink'; static const String inAudioFile = 'in.audioFile'; + /// 双声道交织 PCM:L=本端 / R=对端(用于 MODE_CALL_RECORD / 设备录音通路)。 + static const String inStereo = 'in.stereo'; static const String outSpeaker = 'out.speaker'; static const String outUplink = 'out.uplink'; static const String outDownlink = 'out.downlink';