Browse Source

修复切换语言的bug修复

weicu
liwei1dao 6 months ago
parent
commit
b2b411c468
  1. 798
      ios/Runner.xcodeproj/project.pbxproj
  2. 20
      ios/Runner/Info.plist
  3. 35
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  4. 152
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/DoubaoE2ETranslateHelper.kt
  5. 40
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
  6. 160
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/DoubaoE2ETranslateHelper.swift

798
ios/Runner.xcodeproj/project.pbxproj

@ -0,0 +1,798 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
185F55DBA6478EEF5FFF7E6A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9C84B0B4E86F91BE0E2BE308 /* Pods_RunnerTests.framework */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
CEB768CA2E7CAF03821CF421 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D6C5F44530DFFD81E47C128 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
096510A210F973A646E99AD8 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
0D6C5F44530DFFD81E47C128 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
1F1F03E72E8BE0810058536B /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; };
2D13EECE4B6C62BD47C22D9D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
52494E4695CE324CC6FA34BC /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
88C1945CFDFAE27F2AC121CE /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9C84B0B4E86F91BE0E2BE308 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9FF5CAECB37ADCA2B74922A7 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
A20A85E4945A321D43982A29 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
F5F0D3DE8CC05490B05F5DA9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
CEB768CA2E7CAF03821CF421 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
FA86459C72DB089ADF958E94 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
185F55DBA6478EEF5FFF7E6A /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
E39A6535EC9F01D38C488F0E /* Pods */,
99D7B3F1955EBC6FC7EA7156 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
1F1F03E72E8BE0810058536B /* RunnerProfile.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
88C1945CFDFAE27F2AC121CE /* Runner.entitlements */,
);
path = Runner;
sourceTree = "<group>";
};
99D7B3F1955EBC6FC7EA7156 /* Frameworks */ = {
isa = PBXGroup;
children = (
0D6C5F44530DFFD81E47C128 /* Pods_Runner.framework */,
9C84B0B4E86F91BE0E2BE308 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
E39A6535EC9F01D38C488F0E /* Pods */ = {
isa = PBXGroup;
children = (
2D13EECE4B6C62BD47C22D9D /* Pods-Runner.debug.xcconfig */,
A20A85E4945A321D43982A29 /* Pods-Runner.release.xcconfig */,
F5F0D3DE8CC05490B05F5DA9 /* Pods-Runner.profile.xcconfig */,
52494E4695CE324CC6FA34BC /* Pods-RunnerTests.debug.xcconfig */,
9FF5CAECB37ADCA2B74922A7 /* Pods-RunnerTests.release.xcconfig */,
096510A210F973A646E99AD8 /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
58E539742EB8665AA62B584E /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
FA86459C72DB089ADF958E94 /* Frameworks */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
E8ABF8289F58977405D6531F /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
DF981F3460DF1EA74F93EF68 /* [CP] Embed Pods Frameworks */,
B8F8941054DC9B48433EDB23 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1640;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
58E539742EB8665AA62B584E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
B8F8941054DC9B48433EDB23 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
DF981F3460DF1EA74F93EF68 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
E8ABF8289F58977405D6531F /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 537SFY28XS;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.saitong.voitrans;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 52494E4695CE324CC6FA34BC /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = K39FB49XKY;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.saitong.voitrans.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9FF5CAECB37ADCA2B74922A7 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = K39FB49XKY;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.saitong.voitrans.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 096510A210F973A646E99AD8 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = K39FB49XKY;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.saitong.voitrans.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = K39FB49XKY;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.saitong.voitrans;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = K39FB49XKY;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.saitong.voitrans;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

20
ios/Runner/Info.plist

@ -54,7 +54,6 @@
<string>voitrans</string>
</array>
</dict>
<!-- PayPal 回跳的自定义 Scheme -->
<dict>
<key>CFBundleURLName</key>
<string>deepvoiceclient.payment</string>
@ -74,10 +73,12 @@
<false/>
<key>FacebookClientToken</key>
<string>193d0a2b1757c41cfa557ea8e8f21c33</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>App-Prefs</string>
<string>prefs</string>
<string>App-Prefs</string>
<string>prefs</string>
<string>qqmusic</string>
<string>weixin</string>
<string>weixinULAPI</string>
@ -98,7 +99,7 @@
<string>fbauth2</string>
<string>fbshareextension</string>
<string>google.navigation</string>
<string>googlemaps</string>
<string>googlemaps</string>
<string>comgooglemaps</string>
<string>baidumap</string>
<string>amapuri</string>
@ -114,7 +115,6 @@
<true/>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
<!-- 为网易云音乐图片域名添加例外 -->
<key>NSExceptionDomains</key>
<dict>
<key>music.126.net</key>
@ -160,10 +160,10 @@
<string>Bluetooth permission is required to connect and manage audio devices</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Bluetooth permission is required to connect and manage audio devices</string>
<key>NSCalendarsUsageDescription</key>
<string>Calendar permission is required to schedule and manage your audio meetings and recording tasks</string>
<key>NSCalendarsFullAccessUsageDescription</key>
<string>Calendar permission is required to schedule and manage your audio meetings and recording tasks</string>
<key>NSCalendarsUsageDescription</key>
<string>Calendar permission is required to schedule and manage your audio meetings and recording tasks</string>
<key>NSCameraUsageDescription</key>
<string>Camera permission is required for QR code scanning and video calling functions</string>
<key>NSContactsUsageDescription</key>
@ -178,12 +178,12 @@
<string>This app uses motion and fitness data to detect when you're walking or moving, which helps optimize voice recognition accuracy and automatically adjust microphone sensitivity for better audio recording quality during different activities.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Photo library permission is required to save recording files and import audio files</string>
<key>NSRemindersUsageDescription</key>
<string>This app needs access to reminders to set calendar reminders</string>
<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to deliver personalized ads to you.</string>
<key>SKAdNetworkItems</key>
<array/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
@ -196,8 +196,6 @@
<array>
<string>1</string>
</array>
<key>NSRemindersUsageDescription</key>
<string>This app needs access to reminders to set calendar reminders</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>

35
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt

@ -38,6 +38,7 @@ import kotlinx.coroutines.sync.withLock
import com.yunqiinnovation.azure_speech.tools.RecordFile
import com.example.astclient.DoubaoE2ETranslateHelper
import com.example.astclient.Config
import java.util.concurrent.atomic.AtomicInteger
/** AzureSpeechPlugin */
class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
@ -101,6 +102,9 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
private var astWsConfig: Config? = null
private var currentAstProvider: String = "azure"
// AST 初始化世代号:每次 dispose / initialize 时自增,被 dispatch 出去的 Main 协程
// 在执行前先比对,发现已经过时就直接 return,避免 1.2s delay 后落到旧 provider 上。
private val astInitGeneration = AtomicInteger(0)
private var iflytekAStarted: Boolean = false
private var iflytekBStarted: Boolean = false
@ -132,6 +136,10 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
private var captureQuality: Int = 80
private val REQUEST_SCREEN_CAPTURE = 9001
private fun nextAstInitGeneration(): Int = astInitGeneration.incrementAndGet()
private fun isAstInitGenerationCurrent(generation: Int): Boolean =
astInitGeneration.get() == generation
/**
* 推送音频到当前选择的 AST 服务 A
*
@ -1136,6 +1144,8 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
"dispose" -> {
try {
// 让所有等待 delay(1200L) 的旧 init 协程在恢复时直接 return,避免落到已 dispose 的 helper 上
nextAstInitGeneration()
FileLogger.d(tag, "释放双向 AST 资源(异步)")
callBPcmPlayer.stop()
GlobalScope.launch(Dispatchers.IO) {
@ -1207,6 +1217,7 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
}
"initialize" -> {
val initGeneration = nextAstInitGeneration()
val provider = call.argument<String>("provider") ?: ""
val supportedLanguages =
call.argument<List<String>>("supportedLanguages") ?: listOf("zh-CN")
@ -1301,6 +1312,9 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
)
GlobalScope.launch(Dispatchers.Main) {
if (!isAstInitGenerationCurrent(initGeneration)) {
result.success(false); return@launch
}
val initA = iflytekAstHelperA?.initialize(
azureConfig = azureConfig,
translationConfig = translationConfig,
@ -1309,6 +1323,9 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
) ?: false
delay(1200L)
if (!isAstInitGenerationCurrent(initGeneration)) {
result.success(false); return@launch
}
val initB = iflytekAstHelperB?.initialize(
azureConfig = azureConfig,
@ -1364,6 +1381,9 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
)
GlobalScope.launch(Dispatchers.Main) {
if (!isAstInitGenerationCurrent(initGeneration)) {
result.success(false); return@launch
}
azureAstHelperA?.initialize(
azureConfig = azureConfig,
translationConfig = translationConfig,
@ -1371,6 +1391,9 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
callback = callbackA
)
delay(1200L)
if (!isAstInitGenerationCurrent(initGeneration)) {
result.success(false); return@launch
}
azureAstHelperB?.initialize(
azureConfig = azureConfig,
translationConfig = translationConfig,
@ -1419,8 +1442,14 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
)
GlobalScope.launch(Dispatchers.Main) {
if (!isAstInitGenerationCurrent(initGeneration)) {
result.success(false); return@launch
}
doubaoAstHelperA?.initialize(cfgA, callbackA)
delay(1200L)
if (!isAstInitGenerationCurrent(initGeneration)) {
result.success(false); return@launch
}
doubaoAstHelperB?.initialize(cfgB, callbackB)
result.success(true)
}
@ -1462,8 +1491,14 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
)
GlobalScope.launch(Dispatchers.Main) {
if (!isAstInitGenerationCurrent(initGeneration)) {
result.success(false); return@launch
}
val initA = bailianAstHelperA?.initialize(bailianConfigA, callbackA) ?: false
delay(1200L)
if (!isAstInitGenerationCurrent(initGeneration)) {
result.success(false); return@launch
}
val initB = bailianAstHelperB?.initialize(bailianConfigB, callbackB) ?: false
result.success(initA && initB)
}

152
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/DoubaoE2ETranslateHelper.kt

@ -11,6 +11,7 @@ import java.io.InputStream
import java.util.UUID
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.*
import data.speech.ast.TranslateRequest
import data.speech.ast.TranslateResponse
@ -61,6 +62,17 @@ class DoubaoE2ETranslateHelper(
private var webSocket: WebSocket? = null
private var sessionId: String = ""
private val sessionLock = Any()
// 同一 helper 实例内多次重连/语言切换的世代号;onOpen/onMessage/onFailure/onClosed
// 拿到 generation 与当前 generation 比较,丢弃旧 listener 的回调,避免它们清掉新 ws 状态。
private val connectionGeneration = AtomicInteger(0)
// 切换瞬间(旧 ws 已关、新 ws 还没 onOpen)暂存上行音频,避免首段语音被丢,
// 等服务端真正下发首条消息(确认 session 已就绪)后再 flush 进 pump 缓冲。
private val pendingAudioData = ArrayDeque<ByteArray>()
private val maxPendingAudioChunks = 120
private val hasServerReadyEvent = AtomicBoolean(false)
private val hasFlushedPendingAfterOpen = AtomicBoolean(false)
private val warmupSilentChunk = ByteArray(640)
private val isDisposed = AtomicBoolean(false)
private val recvAudio = ByteArrayOutputStream()
private val recvText = mutableListOf<String>()
@ -90,12 +102,13 @@ class DoubaoE2ETranslateHelper(
fun initialize(config: Config, cb: Callback): Boolean {
instanceTag = "${config.sourceLanguage}->${config.targetLanguage}"
Log.d(TAG, "[$instanceTag] initialize: wsUrl=${config.wsUrl}, resourceId=${config.resourceId}")
// 重置状态,确保重新初始化时能正常启动
isStarted.set(false)
try { webSocket?.close(1000, "reinitialize") } catch (_: Exception) {}
webSocket = null
// 重新初始化时保留切换窗口内已经入队的音频,避免延迟 init 导致首段语音丢失
closeSocketAndResetState("reinitialize", 1000, clearPending = false)
client?.dispatcher?.cancelAll()
client?.connectionPool?.evictAll()
conf = config
callback = cb
isDisposed.set(false)
client = OkHttpClient.Builder()
.pingInterval(30, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.SECONDS)
@ -109,15 +122,18 @@ class DoubaoE2ETranslateHelper(
* 重启会话(用于超时自动重连)
*/
private fun restartSession() {
if (isDisposed.get()) {
Log.i(TAG, "restartSession: already disposed, skip")
return
}
Log.i(TAG, "restartSession: performing auto-restart...")
stopAudioPump()
try { webSocket?.close(1000, "restarting") } catch (_: Exception) {}
webSocket = null
isStarted.set(false)
// 自动重连:保留待发音频,等新 session ready 后再 flush
closeSocketAndResetState("restarting", 1000, clearPending = false)
try {
java.util.Timer().schedule(object : java.util.TimerTask() {
override fun run() {
if (isDisposed.get()) return
Log.i(TAG, "restartSession: timer task running")
if (!isStarted.get()) {
Log.i(TAG, "restartSession: calling startContinuousTranslation")
@ -157,6 +173,7 @@ class DoubaoE2ETranslateHelper(
.header("X-Api-Connect-Id", connId)
.build()
val listenerGeneration = connectionGeneration.incrementAndGet()
val listener = object : WebSocketListener() {
/**
* WebSocket 连接建立时回调。
@ -167,12 +184,20 @@ class DoubaoE2ETranslateHelper(
* @param response 握手响应,包含状态码与头信息
*/
override fun onOpen(ws: WebSocket, response: Response) {
if (listenerGeneration != connectionGeneration.get()) {
Log.i(TAG, "onOpen: stale listener (gen=$listenerGeneration cur=${connectionGeneration.get()}), closing")
try { ws.close(1000, "stale-open") } catch (_: Exception) {}
return
}
Log.d(TAG, "[$instanceTag] onOpen: code=${response.code} logid=${response.header("X-Tt-Logid")}")
webSocket = ws
isStarted.set(true)
hasServerReadyEvent.set(false)
hasFlushedPendingAfterOpen.set(false)
val startReq = makeStartRequest(sessionId)
ws.send(ByteString.of(*startReq.toByteArray()))
Log.d(TAG, "onOpen: StartSession sent")
isStarted.set(true)
sendWarmupPacket(ws)
startAudioPump()
callback?.onSessionStarted(sessionId)
}
@ -189,6 +214,15 @@ class DoubaoE2ETranslateHelper(
* @param bytes 服务端下发的二进制消息
*/
override fun onMessage(ws: WebSocket, bytes: ByteString) {
if (ws !== webSocket) {
Log.i(TAG, "onMessage: ignore stale websocket callback")
return
}
if (hasServerReadyEvent.compareAndSet(false, true) &&
hasFlushedPendingAfterOpen.compareAndSet(false, true)
) {
flushPendingAudioToBuffer()
}
Log.d(TAG, "[$instanceTag] onMessage: ${bytes.size} bytes")
val resp = TranslateResponse.parseFrom(bytes.toByteArray())
val event = resp.event
@ -287,8 +321,13 @@ class DoubaoE2ETranslateHelper(
* @param response 可选的响应对象(可能为 null)
*/
override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) {
if (ws !== webSocket) {
Log.i(TAG, "onFailure: ignore stale websocket callback")
return
}
val msg = t.message ?: "unknown"
Log.e(TAG, "onFailure: ${msg} code=${response?.code}")
markDisconnected(ws)
callback?.onSessionError(sessionId, 1011, msg)
}
@ -301,10 +340,13 @@ class DoubaoE2ETranslateHelper(
* @param reason 关闭原因描述
*/
override fun onClosed(ws: WebSocket, code: Int, reason: String) {
if (ws !== webSocket) {
Log.i(TAG, "onClosed: ignore stale websocket callback")
return
}
Log.d(TAG, "onClosed: code=${code} reason=${reason}")
stopAudioPump()
try { recvAudio.close() } catch (_: Exception) {}
isStarted.set(false)
markDisconnected(ws)
}
}
@ -356,13 +398,21 @@ class DoubaoE2ETranslateHelper(
fun pushAudioData(data: ByteArray): Boolean {
pushCount++
if (pushCount % 100 == 1L) {
Log.d(TAG, "pushAudioData: size=${data.size} isStarted=${isStarted.get()} wsIsNull=${webSocket==null} count=$pushCount")
Log.d(TAG, "pushAudioData: size=${data.size} isStarted=${isStarted.get()} wsIsNull=${webSocket==null} count=$pushCount pending=${pendingAudioData.size}")
}
if (!isStarted.get()) return false
if (data.isNotEmpty()) {
synchronized(audioSendBufferLock) {
audioSendBuffer += data
if (data.isEmpty()) return true
// 切换/重连窗口:ws 还没就绪 → 暂存到 pendingAudioData,等 onMessage 里第一条服务端消息到达再 flush
if (!isStarted.get() || webSocket == null) {
synchronized(sessionLock) {
while (pendingAudioData.size >= maxPendingAudioChunks) {
pendingAudioData.removeFirst()
}
pendingAudioData.addLast(data)
}
return true
}
synchronized(audioSendBufferLock) {
audioSendBuffer += data
}
return true
}
@ -436,16 +486,78 @@ class DoubaoE2ETranslateHelper(
/**
* 释放资源,关闭会话。
* 注意:plugin 持有 helper 单例并在切换语言时反复 initialize/dispose,因此 dispose **不能**
* 调 scope.cancel() —— SupervisorJob 一旦 cancel 永远不可重启,下次 initialize 后再 launch
* 出来的 pumpJob 立刻是 cancelled 状态,body 永不执行,导致服务端 [Timeout waiting next packet]。
* 这里只关 ws、清状态、停 pump。如果将来真要彻底销毁实例,请显式调用 finalDispose()。
*/
fun dispose() {
Log.d(TAG, "dispose: begin")
isDisposed.set(true)
closeSocketAndResetState("dispose", 1000, clearPending = true)
try { recvAudio.close() } catch (_: Exception) {}
client?.dispatcher?.cancelAll()
client?.connectionPool?.evictAll()
Log.d(TAG, "dispose: done (scope kept alive for re-initialize)")
}
/**
* 关闭当前 ws 并重置状态。clearPending=false 时保留 pendingAudioData,
* 用于 reinitialize / restartSession 这类期望延续上行音频的场景。
*/
private fun closeSocketAndResetState(reason: String, code: Int, clearPending: Boolean) {
stopAudioPump()
try { webSocket?.close(1000, "dispose") } catch (_: Exception) {}
try { webSocket?.close(code, reason) } catch (_: Exception) {}
webSocket = null
isStarted.set(false)
try { recvAudio.close() } catch (_: Exception) {}
scope.cancel()
Log.d(TAG, "dispose: done")
hasServerReadyEvent.set(false)
hasFlushedPendingAfterOpen.set(false)
if (clearPending) {
synchronized(sessionLock) { pendingAudioData.clear() }
}
}
/**
* onClosed/onFailure 调用:仅当当前 ws 引用就是回调里那个 ws 时才清空。
* 防止旧 ws 的 onClosed 把新 ws 状态清掉。
*/
private fun markDisconnected(ws: WebSocket) {
if (webSocket === ws) {
webSocket = null
isStarted.set(false)
}
}
/**
* 把 pendingAudioData 一次性灌入 pump 的 audioSendBuffer,让后续 20ms 节拍发送。
*/
private fun flushPendingAudioToBuffer() {
val pending: List<ByteArray>
synchronized(sessionLock) {
if (pendingAudioData.isEmpty()) return
pending = pendingAudioData.toList()
pendingAudioData.clear()
}
Log.d(TAG, "flushPendingAudioToBuffer: count=${pending.size}")
synchronized(audioSendBufferLock) {
for (chunk in pending) {
audioSendBuffer += chunk
}
}
}
/**
* onOpen 后立即先发一帧静音,作为服务端"已收到客户端首包"的探测,
* 防止首条上行被服务端 stream timeout([Timeout waiting next packet])。
*/
private fun sendWarmupPacket(ws: WebSocket) {
try {
val req = makeChunkRequest(sessionId, warmupSilentChunk)
val ok = ws.send(ByteString.of(*req.toByteArray()))
Log.d(TAG, "sendWarmupPacket: sent=$ok size=${warmupSilentChunk.size}")
} catch (e: Exception) {
Log.w(TAG, "sendWarmupPacket: failed", e)
}
}
/**

40
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift

@ -103,6 +103,19 @@ import ble_service
private var bailianAstHelperA = AliyunBailianE2EHelper()
private var bailianAstHelperB = AliyunBailianE2EHelper()
private var currentAstProvider: String = "azure"
// AST 初始化世代号:每次 dispose / initialize 自增;异步任务在恢复时比对,过期就 return,
// 避免新 provider 已切换后旧 init 的延迟代码再落到当前 helper 上。
private var astInitGeneration: Int = 0
private let astInitGenerationLock = NSLock()
private func nextAstInitGeneration() -> Int {
astInitGenerationLock.lock(); defer { astInitGenerationLock.unlock() }
astInitGeneration += 1
return astInitGeneration
}
private func isAstInitGenerationCurrent(_ generation: Int) -> Bool {
astInitGenerationLock.lock(); defer { astInitGenerationLock.unlock() }
return astInitGeneration == generation
}
private var astWsConfig: DoubaoE2ETranslateHelper.Config?
internal var doubaoFinalSourceTextCache: [String: String] = [:]
private var doubaoCallbackA: DoubaoCallbackProxy?
@ -863,6 +876,8 @@ private func sendAudioDataEvent(_ event: [String: Any]) {
result(true)
case "dispose":
// 让 init 的 Task 在 sleep 之后比对 generation 时直接 return,避免落到已 dispose 的 helper 上
_ = nextAstInitGeneration()
os_log("[AST-DISPOSE] Releasing all AST resources, provider=%{public}@", log: ctLog, type: .info, currentAstProvider)
azureAstHelperA.dispose()
azureAstHelperB.dispose()
@ -893,6 +908,7 @@ private func sendAudioDataEvent(_ event: [String: Any]) {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing arguments", details: nil))
return
}
let initGeneration = nextAstInitGeneration()
let provider = (args["provider"] as? String ?? "azure").lowercased()
let supported = (args["supportedLanguages"] as? [String]) ?? ["zh-CN"]
let lang0 = supported.count > 0 ? supported[0] : "zh-CN"
@ -963,10 +979,18 @@ private func sendAudioDataEvent(_ event: [String: Any]) {
let translationConfigI = TranslationConfiguration(region: azureTranslationRegion, subscriptionKey: azureTranslationKey, location: azureTranslationRegion)
Task {
if !self.isAstInitGenerationCurrent(initGeneration) {
DispatchQueue.main.async { result(false) }
return
}
os_log("[AST-INIT] Azure: initializing A service...", log: self.ctLog, type: .info)
await azureAstHelperA.initialize(azureConfig: azureConfigI, translationConfig: translationConfigI, serviceConfig: svcA, callback: callbackA)
os_log("[AST-INIT] Azure: A initialized, waiting 1.2s before B...", log: self.ctLog, type: .info)
try? await Task.sleep(nanoseconds: 1_200_000_000)
if !self.isAstInitGenerationCurrent(initGeneration) {
DispatchQueue.main.async { result(false) }
return
}
os_log("[AST-INIT] Azure: initializing B service...", log: self.ctLog, type: .info)
await azureAstHelperB.initialize(azureConfig: azureConfigI, translationConfig: translationConfigI, serviceConfig: svcB, callback: callbackB)
os_log("[AST-INIT] Azure: A+B both initialized", log: self.ctLog, type: .info)
@ -990,10 +1014,18 @@ private func sendAudioDataEvent(_ event: [String: Any]) {
cfgB.targetLanguage = translationLang0
Task {
if !self.isAstInitGenerationCurrent(initGeneration) {
DispatchQueue.main.async { result(false) }
return
}
os_log("[AST-INIT] Volcano: initializing A service...", log: self.ctLog, type: .info)
let rA = await doubaoAstHelperA.initialize(config: cfgA, cb: cbA)
os_log("[AST-INIT] Volcano: A service result=%d, sleeping 500ms before B...", log: self.ctLog, type: .info, rA ? 1 : 0)
try? await Task.sleep(nanoseconds: 500_000_000)
if !self.isAstInitGenerationCurrent(initGeneration) {
DispatchQueue.main.async { result(false) }
return
}
os_log("[AST-INIT] Volcano: initializing B service...", log: self.ctLog, type: .info)
let rB = await doubaoAstHelperB.initialize(config: cfgB, cb: cbB)
os_log("[AST-INIT] Volcano: B service result=%d. Both services ready.", log: self.ctLog, type: .info, rB ? 1 : 0)
@ -1027,9 +1059,17 @@ private func sendAudioDataEvent(_ event: [String: Any]) {
bailianCallbackB = cbB
Task {
if !self.isAstInitGenerationCurrent(initGeneration) {
DispatchQueue.main.async { result(false) }
return
}
os_log("[AST-INIT] Alibaba: initializing A service...", log: self.ctLog, type: .info)
let rA = await bailianAstHelperA.initialize(config: cfgA, cb: cbA)
os_log("[AST-INIT] Alibaba: A service result=%d, initializing B service...", log: self.ctLog, type: .info, rA ? 1 : 0)
if !self.isAstInitGenerationCurrent(initGeneration) {
DispatchQueue.main.async { result(false) }
return
}
let rB = await bailianAstHelperB.initialize(config: cfgB, cb: cbB)
os_log("[AST-INIT] Alibaba: B service result=%d. Both services ready.", log: self.ctLog, type: .info, rB ? 1 : 0)
DispatchQueue.main.async { result(true) }

160
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/DoubaoE2ETranslateHelper.swift

@ -53,9 +53,15 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate {
private var audioChunkBuffer = Data()
private var isStarted = false
private var isDisposed = false
private let lock = NSLock()
private var isWebSocketConnected = false
// 切换/重连窗口内(urlSession 替换、新 ws 还没 didOpen)暂存上行音频,
// 等 onMessage 收到第一条服务端消息再 flush 到 pump 缓冲。
private var pendingAudioData: [Data] = []
private let maxPendingAudioChunks = 120
private var hasFlushedPendingAfterOpen = false
// 上行音频 pump:
// 会话建立后启动一个 20ms 固定节拍的定时器,持续从 audioSendBuffer 取 640B 一帧上行;
@ -74,7 +80,15 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate {
* 返回是否成功触发了连接逻辑
*/
func initialize(config: Config, cb: Callback) async -> Bool {
// 清理旧的 Session
// 重新初始化:保留 pendingAudioData,等新 ws 起来后 flush;只清状态/旧 session
stopAudioPump()
webSocket?.cancel(with: .normalClosure, reason: "reinitialize".data(using: .utf8))
webSocket = nil
isStarted = false
lock.lock()
isWebSocketConnected = false
hasFlushedPendingAfterOpen = false
lock.unlock()
if let session = urlSession {
session.invalidateAndCancel()
}
@ -87,6 +101,7 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate {
conf.resourceId = conf.resourceId.trimmingCharacters(in: .whitespacesAndNewlines)
callback = cb
isDisposed = false
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 0 // 恢复为0(无超时),因为 WebSocket 是长连接
configuration.timeoutIntervalForResource = 0
@ -102,6 +117,10 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate {
* 重启会话(用于超时自动重连)
*/
private func restartSession() {
if isDisposed {
os_log("restartSession: already disposed, skip", log: log, type: .info)
return
}
os_log("Restarting session due to timeout...", log: log, type: .info)
stopAudioPump()
@ -110,10 +129,12 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate {
isStarted = false
lock.lock()
isWebSocketConnected = false
hasFlushedPendingAfterOpen = false
// 保留 pendingAudioData:自动重连仍想把切换瞬间的麦克风音频送出
lock.unlock()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
guard let self = self else { return }
guard let self = self, !self.isDisposed else { return }
if !self.isStarted {
_ = self.startContinuousTranslation()
}
@ -177,20 +198,80 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate {
/**
* 只负责把外部音频塞进上行缓冲区,真正的发送由 pump 按 20ms 节拍统一调度。
* 切换/重连窗口(isStarted=false 或 ws 还没建好):先入 pendingAudioData,
* 等首条服务端消息到达再 flush 进 audioSendBuffer。
*/
func pushAudioData(_ data: Data) -> Bool {
guard isStarted else {
os_log("Push audio failed: not started", log: log, type: .error)
return false
}
if !data.isEmpty {
audioSendBufferLock.lock()
audioSendBuffer.append(data)
audioSendBufferLock.unlock()
if isDisposed { return false }
if data.isEmpty { return true }
if !isStarted || !isWebSocketConnected {
lock.lock()
while pendingAudioData.count >= maxPendingAudioChunks {
pendingAudioData.removeFirst()
}
pendingAudioData.append(data)
lock.unlock()
return true
}
audioSendBufferLock.lock()
audioSendBuffer.append(data)
audioSendBufferLock.unlock()
return true
}
/**
* 把 pendingAudioData 一次性灌入 audioSendBuffer,让 pump 按 20ms 节拍消费。
*/
private func flushPendingAudioToBuffer() {
var pending: [Data] = []
lock.lock()
if hasFlushedPendingAfterOpen || pendingAudioData.isEmpty {
lock.unlock()
return
}
hasFlushedPendingAfterOpen = true
pending = pendingAudioData
pendingAudioData.removeAll()
lock.unlock()
os_log("flushPendingAudioToBuffer: count=%{public}d", log: log, type: .info, pending.count)
audioSendBufferLock.lock()
for chunk in pending {
audioSendBuffer.append(chunk)
}
audioSendBufferLock.unlock()
}
/**
* onOpen 后立即先发一帧静音,作为服务端"已收到客户端首包"的探测,
* 防止服务端 stream timeout([Timeout waiting next packet])。
*/
private func sendWarmupPacket(_ ws: URLSessionWebSocketTask) {
let req = makeChunkRequest(sessionId: sessionId, chunk: silentFrame)
guard let msg = try? req.serializedData() else { return }
ws.send(.data(msg)) { [weak self] error in
if let e = error {
os_log("sendWarmupPacket failed: %{public}@", log: self?.log ?? .default, type: .info, e.localizedDescription)
}
}
}
/**
* 仅当当前 ws 引用就是回调里那个 ws 时才清空,避免旧 ws 的 close 把新 ws 状态清掉。
*/
private func markDisconnected(currentWs: URLSessionWebSocketTask) {
lock.lock()
let isCurrent = (webSocket === currentWs)
if isCurrent {
webSocket = nil
isWebSocketConnected = false
}
lock.unlock()
if isCurrent {
isStarted = false
stopAudioPump()
}
}
/**
* 从上行缓冲区取一帧 20ms 数据;不够则返回静音帧。
*/
@ -271,16 +352,24 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate {
}
/**
* 释放资源
* 释放资源。
* 注意:对齐 Android 行为——plugin 持有 helper 单例并在切换语言时反复 initialize/dispose,
* 因此这里**只清状态**,不能让 helper 进入"永久不可用"状态:
* - pumpTimer/urlSession 都是按需重建的(`startAudioPump`/`initialize` 会重新创建),不需要 deinit pumpQueue
* - isDisposed=true 只在"dispose 之后到下次 initialize 之前"这段窗口生效,
* 用于让在飞的 restartSession timer 跳过;initialize 会把 isDisposed 重置为 false
*/
func dispose() {
os_log("Disposing resources", log: log, type: .info)
os_log("Disposing resources (instance reusable after re-initialize)", log: log, type: .info)
isDisposed = true
stopAudioPump()
do { try webSocket?.cancel(with: .normalClosure, reason: "dispose".data(using: .utf8)) } catch { }
webSocket?.cancel(with: .normalClosure, reason: "dispose".data(using: .utf8))
webSocket = nil
isStarted = false
lock.lock()
isWebSocketConnected = false
pendingAudioData.removeAll()
hasFlushedPendingAfterOpen = false
lock.unlock()
urlSession?.invalidateAndCancel()
urlSession = nil
@ -290,6 +379,10 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate {
* WebSocket打开回调(发送 Protobuf StartSession)
*/
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
guard session === urlSession, webSocketTask === webSocket else {
os_log("Ignore didOpen for stale URLSession/WebSocket", log: log, type: .info)
return
}
os_log("WebSocket didOpen", log: log, type: .info)
let startReq = makeStartRequest(sessionId: sessionId)
if let data = try? startReq.serializedData() {
@ -303,9 +396,11 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate {
}
}
}
sendWarmupPacket(webSocketTask)
lock.lock()
isWebSocketConnected = true
hasFlushedPendingAfterOpen = false
lock.unlock()
startAudioPump()
@ -318,28 +413,35 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate {
* WebSocket关闭回调
*/
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
guard session === urlSession else {
os_log("Ignore didClose for stale URLSession", log: log, type: .info)
return
}
let reasonStr = String(data: reason ?? Data(), encoding: .utf8) ?? ""
os_log("WebSocket didClose, code: %d, reason: %{public}@", log: log, type: .info, closeCode.rawValue, reasonStr)
stopAudioPump()
lock.lock()
isWebSocketConnected = false
lock.unlock()
isStarted = false
markDisconnected(currentWs: webSocketTask)
}
/**
* 任务完成回调(处理错误)
*/
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
// initialize 会 invalidate 旧 session 并新建;旧 task 的完成回调必须忽略,否则会误伤当前连接
guard session === urlSession else {
return
}
if let wsTask = task as? URLSessionWebSocketTask, wsTask !== webSocket {
return
}
if let e = error {
os_log("WebSocket task failed: %{public}@", log: log, type: .error, e.localizedDescription)
callback?.onSessionError(sessionId: sessionId, code: 1012, message: e.localizedDescription)
stopAudioPump()
lock.lock()
isWebSocketConnected = false
lock.unlock()
isStarted = false
if let wsTask = task as? URLSessionWebSocketTask {
markDisconnected(currentWs: wsTask)
} else {
stopAudioPump()
isStarted = false
}
}
}
@ -347,12 +449,20 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate {
* 接收消息循环
*/
private func receiveLoop() {
webSocket?.receive { [weak self] result in
guard let receivingWs = webSocket else { return }
receivingWs.receive { [weak self] result in
guard let self = self else { return }
// 语言切换 / 重连时旧 ws 的 receive 可能晚于新 ws 建立;忽略 stale 回调,否则会误关新连接
guard self.webSocket === receivingWs else {
os_log("Ignore receive for stale WebSocket", log: self.log, type: .info)
return
}
switch result {
case .failure(let error):
self.callback?.onSessionError(sessionId: self.sessionId, code: 1011, message: error.localizedDescription)
case .success(let message):
// 首条服务端消息抵达即认为 session 真正就绪,把切换瞬间缓冲的 pending 灌给 pump
self.flushPendingAudioToBuffer()
self.handleMessage(message)
self.receiveLoop()
}

Loading…
Cancel
Save