Browse Source

上传杰里sdk 代码

weicu
liwei1dao 5 months ago
parent
commit
42e004dcad
  1. 20
      lib/data/services/ble_manager.dart
  2. 8
      lib/modules/translation/controllers/translation_controller.dart
  3. 117
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
  4. 28
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/DoubaoE2ETranslateHelper.swift
  5. 15
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/Info.plist
  6. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64/JLBmpConvertKit.framework/Info.plist
  7. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64/JLBmpConvertKit.framework/JLBmpConvertKit
  8. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64_x86_64-simulator/JLBmpConvertKit.framework/Info.plist
  9. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64_x86_64-simulator/JLBmpConvertKit.framework/JLBmpConvertKit
  10. 2
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64_x86_64-simulator/JLBmpConvertKit.framework/_CodeSignature/CodeResources
  11. 1
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Headers
  12. 1
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/JLBmpConvertKit
  13. 1
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Modules
  14. 1
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Resources
  15. 95
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLBmpConvert.h
  16. 24
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLBmpConvertKit.h
  17. 89
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLBmpConvertOption.h
  18. 77
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLGifBin.h
  19. 34
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Headers/JLImageConvertResult.h
  20. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/JLBmpConvertKit
  21. 6
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Modules/module.modulemap
  22. 48
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/Resources/Info.plist
  23. 1
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/Current
  24. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64/JLDialUnit.framework/Info.plist
  25. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64/JLDialUnit.framework/JLDialUnit
  26. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64_x86_64-simulator/JLDialUnit.framework/Info.plist
  27. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64_x86_64-simulator/JLDialUnit.framework/JLDialUnit
  28. 2
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64_x86_64-simulator/JLDialUnit.framework/_CodeSignature/CodeResources
  29. 15
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/Info.plist
  30. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64/JLLogHelper.framework/Info.plist
  31. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64/JLLogHelper.framework/JLLogHelper
  32. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64_x86_64-simulator/JLLogHelper.framework/Info.plist
  33. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64_x86_64-simulator/JLLogHelper.framework/JLLogHelper
  34. 2
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64_x86_64-simulator/JLLogHelper.framework/_CodeSignature/CodeResources
  35. 1
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Headers
  36. 1
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/JLLogHelper
  37. 1
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Modules
  38. 1
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Resources
  39. 19
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/Headers/JLLogHelper.h
  40. 88
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/Headers/JLLogManager.h
  41. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/JLLogHelper
  42. 6
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/Modules/module.modulemap
  43. 48
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/Resources/Info.plist
  44. 1
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/Current
  45. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLPackageResKit.xcframework/ios-arm64/JLPackageResKit.framework/JLPackageResKit
  46. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JLPackageResKit.xcframework/ios-arm64_x86_64-simulator/JLPackageResKit.framework/JLPackageResKit
  47. 10
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/Info.plist
  48. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64/JL_AdvParse.framework/Info.plist
  49. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64/JL_AdvParse.framework/JL_AdvParse
  50. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64_x86_64-simulator/JL_AdvParse.framework/Info.plist
  51. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64_x86_64-simulator/JL_AdvParse.framework/JL_AdvParse
  52. 2
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64_x86_64-simulator/JL_AdvParse.framework/_CodeSignature/CodeResources
  53. 23
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64/JL_BLEKit.framework/Headers/JLDialInfoExtentedModel.h
  54. 1
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64/JL_BLEKit.framework/Headers/JL_Assist.h
  55. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64/JL_BLEKit.framework/JL_BLEKit
  56. 23
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/Headers/JLDialInfoExtentedModel.h
  57. 1
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/Headers/JL_Assist.h
  58. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/JL_BLEKit
  59. 12
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/_CodeSignature/CodeResources
  60. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64/JL_HashPair.framework/Info.plist
  61. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64/JL_HashPair.framework/JL_HashPair
  62. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64_x86_64-simulator/JL_HashPair.framework/Info.plist
  63. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64_x86_64-simulator/JL_HashPair.framework/JL_HashPair
  64. 2
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64_x86_64-simulator/JL_HashPair.framework/_CodeSignature/CodeResources
  65. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64/JL_OTALib.framework/Info.plist
  66. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64/JL_OTALib.framework/JL_OTALib
  67. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64_x86_64-simulator/JL_OTALib.framework/Info.plist
  68. BIN
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64_x86_64-simulator/JL_OTALib.framework/JL_OTALib
  69. 2
      local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64_x86_64-simulator/JL_OTALib.framework/_CodeSignature/CodeResources
  70. 9
      local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/MethodRouter.swift
  71. 81
      local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/audio/OpusStreamCodec.swift
  72. 2
      local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/core/TranslationSession.swift
  73. 64
      local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/ConnectFeature.swift
  74. 28
      local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/DeviceRecordFeature.swift
  75. 17
      local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/TranslationFeature.swift
  76. 6
      local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/translation/TranslationConstants.swift
  77. 191
      local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/translation/mode/CallRecordModeHandler.swift
  78. 286
      local_plugins/device_jieli/ios/device_jieli/Sources/device_jieli/feature/translation/mode/CallTranslationModeHandler.swift
  79. 6
      local_plugins/device_jieli/lib/device_jieli.dart

20
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) {

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

117
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..<count {
let s = Double(p[i])
sumSq += s * s
}
}
return (sumSq / Double(count)).squareRoot()
}
/// 取前 N 字节 hex 预览(诊断是否全 0)。
private static func hexPreview(_ data: Data, _ n: Int) -> String {
let take = min(n, data.count)
if take == 0 { return "<empty>" }
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,

28
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)

15
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/Info.plist

@ -18,6 +18,21 @@
<key>SupportedPlatform</key>
<string>ios</string>
</dict>
<dict>
<key>BinaryPath</key>
<string>JLBmpConvertKit.framework/Versions/A/JLBmpConvertKit</string>
<key>LibraryIdentifier</key>
<string>macos-arm64_x86_64</string>
<key>LibraryPath</key>
<string>JLBmpConvertKit.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>macos</string>
</dict>
<dict>
<key>BinaryPath</key>
<string>JLBmpConvertKit.framework/JLBmpConvertKit</string>

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64/JLBmpConvertKit.framework/Info.plist

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64/JLBmpConvertKit.framework/JLBmpConvertKit

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64_x86_64-simulator/JLBmpConvertKit.framework/Info.plist

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64_x86_64-simulator/JLBmpConvertKit.framework/JLBmpConvertKit

Binary file not shown.

2
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/ios-arm64_x86_64-simulator/JLBmpConvertKit.framework/_CodeSignature/CodeResources

@ -26,7 +26,7 @@
</data>
<key>Info.plist</key>
<data>
mH2wyDJH099CSL7ndYKtKlBiJ3M=
yF0Q5ofGeBGl7TtNvUrICUVzXe8=
</data>
<key>Modules/module.modulemap</key>
<data>

1
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Headers

@ -0,0 +1 @@
Versions/Current/Headers

1
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/JLBmpConvertKit

@ -0,0 +1 @@
Versions/Current/JLBmpConvertKit

1
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Modules

@ -0,0 +1 @@
Versions/Current/Modules

1
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Resources

@ -0,0 +1 @@
Versions/Current/Resources

95
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 <Foundation/Foundation.h>
#import <JLBmpConvertKit/JLBmpConvertKit.h>
#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
#import <UIKit/UIKit.h>
#elif TARGET_OS_OSX
#import <AppKit/AppKit.h>
#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

24
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 <Foundation/Foundation.h>
//! 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 <JLBmpConvertKit/PublicHeader.h>
#import <JLBmpConvertKit/JLBmpConvertOption.h>
#import <JLBmpConvertKit/JLBmpConvert.h>
#import <JLBmpConvertKit/JLGifBin.h>
#import <JLBmpConvertKit/JLImageConvertResult.h>

89
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 <Foundation/Foundation.h>
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

77
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 <Foundation/Foundation.h>
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

34
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 <Foundation/Foundation.h>
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

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/A/JLBmpConvertKit

Binary file not shown.

6
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 * }
}

48
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 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>25B78</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>JLBmpConvertKit</string>
<key>CFBundleIdentifier</key>
<string>com.zh-jieli.JLBmpConvertKit</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>JLBmpConvertKit</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.5.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>25C57</string>
<key>DTPlatformName</key>
<string>macosx</string>
<key>DTPlatformVersion</key>
<string>26.2</string>
<key>DTSDKBuild</key>
<string>25C57</string>
<key>DTSDKName</key>
<string>macosx26.2</string>
<key>DTXcode</key>
<string>2620</string>
<key>DTXcodeBuild</key>
<string>17C52</string>
<key>LSMinimumSystemVersion</key>
<string>10.13</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2024 www.zh-jieli.com. All rights reserved.</string>
</dict>
</plist>

1
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLBmpConvertKit.xcframework/macos-arm64_x86_64/JLBmpConvertKit.framework/Versions/Current

@ -0,0 +1 @@
A

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64/JLDialUnit.framework/Info.plist

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64/JLDialUnit.framework/JLDialUnit

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64_x86_64-simulator/JLDialUnit.framework/Info.plist

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64_x86_64-simulator/JLDialUnit.framework/JLDialUnit

Binary file not shown.

2
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLDialUnit.xcframework/ios-arm64_x86_64-simulator/JLDialUnit.framework/_CodeSignature/CodeResources

@ -50,7 +50,7 @@
</data>
<key>Info.plist</key>
<data>
S01R8qKENWyeFMKwZu8s9ypBNCQ=
UOVQW+J5hxFv4GqeGxCjfcQ+XtE=
</data>
<key>Modules/module.modulemap</key>
<data>

15
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/Info.plist

@ -4,6 +4,21 @@
<dict>
<key>AvailableLibraries</key>
<array>
<dict>
<key>BinaryPath</key>
<string>JLLogHelper.framework/Versions/A/JLLogHelper</string>
<key>LibraryIdentifier</key>
<string>macos-arm64_x86_64</string>
<key>LibraryPath</key>
<string>JLLogHelper.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>macos</string>
</dict>
<dict>
<key>BinaryPath</key>
<string>JLLogHelper.framework/JLLogHelper</string>

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64/JLLogHelper.framework/Info.plist

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64/JLLogHelper.framework/JLLogHelper

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64_x86_64-simulator/JLLogHelper.framework/Info.plist

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64_x86_64-simulator/JLLogHelper.framework/JLLogHelper

Binary file not shown.

2
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/ios-arm64_x86_64-simulator/JLLogHelper.framework/_CodeSignature/CodeResources

@ -14,7 +14,7 @@
</data>
<key>Info.plist</key>
<data>
PJl+BYy7qbdneBUnydYicJKb/SM=
mJ09lIigUxIgwpwz1tMqaHs8roI=
</data>
<key>Modules/module.modulemap</key>
<data>

1
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Headers

@ -0,0 +1 @@
Versions/Current/Headers

1
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/JLLogHelper

@ -0,0 +1 @@
Versions/Current/JLLogHelper

1
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Modules

@ -0,0 +1 @@
Versions/Current/Modules

1
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Resources

@ -0,0 +1 @@
Versions/Current/Resources

19
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 <Foundation/Foundation.h>
//! 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 <JLLogHelper/PublicHeader.h>
#import <JLLogHelper/JLLogManager.h>

88
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 <Foundation/Foundation.h>
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

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/A/JLLogHelper

Binary file not shown.

6
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 * }
}

48
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 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>25B78</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>JLLogHelper</string>
<key>CFBundleIdentifier</key>
<string>com.zh-jieli.JLLogHelper</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>JLLogHelper</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.2</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>25C57</string>
<key>DTPlatformName</key>
<string>macosx</string>
<key>DTPlatformVersion</key>
<string>26.2</string>
<key>DTSDKBuild</key>
<string>25C57</string>
<key>DTSDKName</key>
<string>macosx26.2</string>
<key>DTXcode</key>
<string>2620</string>
<key>DTXcodeBuild</key>
<string>17C52</string>
<key>LSMinimumSystemVersion</key>
<string>26.2</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2024 www.zh-jieli.com. All rights reserved.</string>
</dict>
</plist>

1
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLLogHelper.xcframework/macos-arm64_x86_64/JLLogHelper.framework/Versions/Current

@ -0,0 +1 @@
A

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLPackageResKit.xcframework/ios-arm64/JLPackageResKit.framework/JLPackageResKit

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JLPackageResKit.xcframework/ios-arm64_x86_64-simulator/JLPackageResKit.framework/JLPackageResKit

Binary file not shown.

10
local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/Info.plist

@ -8,32 +8,32 @@
<key>BinaryPath</key>
<string>JL_AdvParse.framework/JL_AdvParse</string>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<string>ios-arm64_x86_64-simulator</string>
<key>LibraryPath</key>
<string>JL_AdvParse.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
<dict>
<key>BinaryPath</key>
<string>JL_AdvParse.framework/JL_AdvParse</string>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>JL_AdvParse.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
</array>
<key>CFBundlePackageType</key>

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64/JL_AdvParse.framework/Info.plist

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64/JL_AdvParse.framework/JL_AdvParse

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64_x86_64-simulator/JL_AdvParse.framework/Info.plist

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_AdvParse.xcframework/ios-arm64_x86_64-simulator/JL_AdvParse.framework/JL_AdvParse

Binary file not shown.

2
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 @@
</data>
<key>Info.plist</key>
<data>
y32GLYYZ4FDhJGOjUkYUZfxaRn8=
Yw+NwkpNMafTbGDLvF8mhT3Alh8=
</data>
<key>Modules/module.modulemap</key>
<data>

23
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

1
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;

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64/JL_BLEKit.framework/JL_BLEKit

Binary file not shown.

23
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

1
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;

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_BLEKit.xcframework/ios-arm64_x86_64-simulator/JL_BLEKit.framework/JL_BLEKit

Binary file not shown.

12
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 @@
</data>
<key>Headers/JLDialInfoExtentedModel.h</key>
<data>
SqF+gwV8xas+qeqllpe46CC0G0k=
u05R1xDr/Goqgu1SLpflf0Okryc=
</data>
<key>Headers/JLDisconnectRemindModel.h</key>
<data>
@ -338,7 +338,7 @@
</data>
<key>Headers/JL_Assist.h</key>
<data>
JmuMMHFAyKbie1xtZpOhO3PEoxk=
JmeqEfLSOWlrtfMvA3oj2ZB4vlA=
</data>
<key>Headers/JL_BLEAction.h</key>
<data>
@ -861,11 +861,11 @@
<dict>
<key>hash</key>
<data>
SqF+gwV8xas+qeqllpe46CC0G0k=
u05R1xDr/Goqgu1SLpflf0Okryc=
</data>
<key>hash2</key>
<data>
tXTQgWqZ6OBex+aPNIsDWzI2fMA80L9JXEwmv8JeEao=
dLH0g5OaY6mg+cEo6jDF9a/xSBcoI/Tt+Ukmbk+2cJE=
</data>
</dict>
<key>Headers/JLDisconnectRemindModel.h</key>
@ -1488,11 +1488,11 @@
<dict>
<key>hash</key>
<data>
JmuMMHFAyKbie1xtZpOhO3PEoxk=
JmeqEfLSOWlrtfMvA3oj2ZB4vlA=
</data>
<key>hash2</key>
<data>
J2dFYlibDG6vMXH366nZsKEAuJyWWFT5POxY7X9txWM=
6QbcztusftVvRuOkEEH+xwPBC+QfHfVjy2JNe4WUOIM=
</data>
</dict>
<key>Headers/JL_BLEAction.h</key>

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64/JL_HashPair.framework/Info.plist

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64/JL_HashPair.framework/JL_HashPair

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64_x86_64-simulator/JL_HashPair.framework/Info.plist

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_HashPair.xcframework/ios-arm64_x86_64-simulator/JL_HashPair.framework/JL_HashPair

Binary file not shown.

2
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 @@
</data>
<key>Info.plist</key>
<data>
pLA2PfPyxsguwM09KFXPq/Cy2ko=
vaP94eof9P4dJHy/VxAhSfezsXw=
</data>
<key>Modules/module.modulemap</key>
<data>

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64/JL_OTALib.framework/Info.plist

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64/JL_OTALib.framework/JL_OTALib

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64_x86_64-simulator/JL_OTALib.framework/Info.plist

Binary file not shown.

BIN
local_plugins/device_jieli/ios/device_jieli/Frameworks/JL_OTALib.xcframework/ios-arm64_x86_64-simulator/JL_OTALib.framework/JL_OTALib

Binary file not shown.

2
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 @@
</data>
<key>Info.plist</key>
<data>
WfMSuIuMgNEEbLR1JZENJAqvP5Y=
L1sSyhac1P15lar1/muRvYYRCUI=
</data>
<key>Modules/module.modulemap</key>
<data>

9
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":

81
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?) {
// 文件级接口不走此回调,留空。
}
}

2
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 时

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

28
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()
}
}

17
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),

6
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 音频规格

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

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

6
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';

Loading…
Cancel
Save