fix:增加ios解码实现
This commit is contained in:
parent
ca95779043
commit
e375c0aeb4
@ -1,4 +1,4 @@
|
||||
org.gradle.jvmargs=-Xmx4G
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
org.gradle.java.home=C:/Users/liyi/other/jdk-17.0.1
|
||||
org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk-17.0.1.jdk/Contents/Home
|
||||
|
||||
41
example/ios/Podfile.lock
Normal file
41
example/ios/Podfile.lock
Normal file
@ -0,0 +1,41 @@
|
||||
PODS:
|
||||
- Flutter (1.0.0)
|
||||
- integration_test (0.0.1):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- permission_handler_apple (9.1.1):
|
||||
- Flutter
|
||||
- video_decode_plugin (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- video_decode_plugin (from `.symlinks/plugins/video_decode_plugin/ios`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
integration_test:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
video_decode_plugin:
|
||||
:path: ".symlinks/plugins/video_decode_plugin/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
integration_test: 2d03ab552da9a1f408709a6acf3d7ca4cb3cb307
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 3787117e48f80715ff04a3830ca039283d6a4f29
|
||||
video_decode_plugin: 07649b4703fdf618daf7000af58f3b251c3e280f
|
||||
|
||||
PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
@ -10,7 +10,9 @@
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
3CE7980F7865F7F8E5B7162F /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 521BE986C2B423CB297B1C00 /* Pods_RunnerTests.framework */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
8D7AB43641B575B850A72098 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F27CD0209D3FA843C2B3E9E /* Pods_Runner.framework */; };
|
||||
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 */; };
|
||||
@ -42,9 +44,15 @@
|
||||
/* Begin PBXFileReference section */
|
||||
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>"; };
|
||||
1F27CD0209D3FA843C2B3E9E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
21DF05E93201EF3DED427347 /* 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>"; };
|
||||
4A2F7419BFF633E97CA4B2ED /* 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>"; };
|
||||
521BE986C2B423CB297B1C00 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6A7EFB3AABB3AB94044A2F39 /* 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>"; };
|
||||
6D4BFC721107664848B40160 /* 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>"; };
|
||||
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>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
@ -55,13 +63,24 @@
|
||||
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>"; };
|
||||
A46450F4F351DEEECD7F5A62 /* 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>"; };
|
||||
A84C216D2CDBF62BB951B792 /* 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 */
|
||||
2AE63D1C6EB40BB145F7AFAE /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3CE7980F7865F7F8E5B7162F /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
8D7AB43641B575B850A72098 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -94,6 +113,8 @@
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
A8D9A7D8E0142B6FF06E6590 /* Pods */,
|
||||
ACF8A28F7E0BB4CDED852419 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@ -121,6 +142,28 @@
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A8D9A7D8E0142B6FF06E6590 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
21DF05E93201EF3DED427347 /* Pods-Runner.debug.xcconfig */,
|
||||
6A7EFB3AABB3AB94044A2F39 /* Pods-Runner.release.xcconfig */,
|
||||
A84C216D2CDBF62BB951B792 /* Pods-Runner.profile.xcconfig */,
|
||||
4A2F7419BFF633E97CA4B2ED /* Pods-RunnerTests.debug.xcconfig */,
|
||||
A46450F4F351DEEECD7F5A62 /* Pods-RunnerTests.release.xcconfig */,
|
||||
6D4BFC721107664848B40160 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ACF8A28F7E0BB4CDED852419 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1F27CD0209D3FA843C2B3E9E /* Pods_Runner.framework */,
|
||||
521BE986C2B423CB297B1C00 /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@ -128,8 +171,10 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
4E3E18A6B5EBAEE36E80FB44 /* [CP] Check Pods Manifest.lock */,
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
2AE63D1C6EB40BB145F7AFAE /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@ -145,12 +190,14 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
ACFD06CDD74A2D9EC586D2CF /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
C1DA649D0A31D2DCC95D2BAA /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@ -238,6 +285,28 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
4E3E18A6B5EBAEE36E80FB44 /* [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;
|
||||
@ -253,6 +322,45 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
ACFD06CDD74A2D9EC586D2CF /* [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;
|
||||
};
|
||||
C1DA649D0A31D2DCC95D2BAA /* [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;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@ -361,16 +469,20 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = U3J8U8WN26;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NAQ5PL2DYC;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = top.skychip.videoDecodePluginExample;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.starlock.lock.local;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = Debug_com.starlock.lock.local.mobileprovision;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@ -379,6 +491,7 @@
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 4A2F7419BFF633E97CA4B2ED /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@ -396,6 +509,7 @@
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A46450F4F351DEEECD7F5A62 /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@ -411,6 +525,7 @@
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 6D4BFC721107664848B40160 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@ -541,16 +656,20 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = U3J8U8WN26;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NAQ5PL2DYC;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = top.skychip.videoDecodePluginExample;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.starlock.lock.local;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = Debug_com.starlock.lock.local.mobileprovision;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@ -564,16 +683,20 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = U3J8U8WN26;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NAQ5PL2DYC;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = top.skychip.videoDecodePluginExample;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.starlock.lock.local;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = Debug_com.starlock.lock.local.mobileprovision;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
|
||||
@ -4,4 +4,7 @@
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
@ -2,6 +2,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>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@ -24,6 +26,8 @@
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
@ -41,9 +45,5 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -281,7 +281,7 @@ class _VideoViewState extends State<VideoView> {
|
||||
setState(() {
|
||||
_h264Frames = frames;
|
||||
});
|
||||
_log("H264文件解析完成,找到 "+frames.length.toString()+" 个帧");
|
||||
_log("H264文件解析完成,找到 " + frames.length.toString() + " 个帧");
|
||||
}
|
||||
|
||||
// 查找起始码的辅助方法
|
||||
@ -303,7 +303,6 @@ class _VideoViewState extends State<VideoView> {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 获取NAL类型
|
||||
// 获取NAL类型
|
||||
int _getNalType(Uint8List data) {
|
||||
// 跳过起始码后再获取NAL单元类型
|
||||
@ -389,6 +388,36 @@ class _VideoViewState extends State<VideoView> {
|
||||
}
|
||||
try {
|
||||
final timestamp = DateTime.now().microsecondsSinceEpoch;
|
||||
// 如果是I帧,先发送SPS和PPS
|
||||
int nalType = _getNalType(frame.data);
|
||||
if (nalType == NalUnitType.CODED_SLICE_IDR) {
|
||||
// 查找最近的SPS和PPS
|
||||
H264Frame? sps, pps;
|
||||
for (int i = frameSeq - 1; i >= 0; i--) {
|
||||
int t = _getNalType(_h264Frames[i].data);
|
||||
if (sps == null && t == NalUnitType.SPS) sps = _h264Frames[i];
|
||||
if (pps == null && t == NalUnitType.PPS) pps = _h264Frames[i];
|
||||
if (sps != null && pps != null) break;
|
||||
}
|
||||
if (sps != null) {
|
||||
await VideoDecodePlugin.sendFrame(
|
||||
frameData: sps.data,
|
||||
frameType: 0,
|
||||
timestamp: timestamp,
|
||||
frameSeq: frameSeq - 2,
|
||||
splitNalFromIFrame: false,
|
||||
);
|
||||
}
|
||||
if (pps != null) {
|
||||
await VideoDecodePlugin.sendFrame(
|
||||
frameData: pps.data,
|
||||
frameType: 0,
|
||||
timestamp: timestamp,
|
||||
frameSeq: frameSeq - 1,
|
||||
splitNalFromIFrame: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
await VideoDecodePlugin.sendFrame(
|
||||
frameData: frame.data,
|
||||
frameType: frame.frameType,
|
||||
@ -506,20 +535,11 @@ class _VideoViewState extends State<VideoView> {
|
||||
if (_currentFrameIndex >= _h264Frames.length) {
|
||||
_log("所有帧已解码,重新开始");
|
||||
_currentFrameIndex = 0;
|
||||
|
||||
// 重新发送SPS和PPS
|
||||
await _sendSpsAndPps();
|
||||
}
|
||||
|
||||
final frame = _h264Frames[_currentFrameIndex];
|
||||
// 未初始化成功时不发送解码帧
|
||||
if (!_isInitialized || _textureId == null) return;
|
||||
await _decodeNextFrame(frame, _currentFrameIndex);
|
||||
|
||||
// 只有在成功解码的情况下才显示日志信息
|
||||
// if (!decodeSuccess && _enablePacketLoss) {
|
||||
// _log("跳过索引 $_currentFrameIndex 的帧(丢帧模拟)");
|
||||
// }
|
||||
|
||||
// 无论解码是否成功,都移动到下一帧
|
||||
_currentFrameIndex++;
|
||||
});
|
||||
}
|
||||
@ -837,7 +857,6 @@ class _VideoViewState extends State<VideoView> {
|
||||
},
|
||||
child: Text('刷新'),
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -6,7 +6,7 @@ packages:
|
||||
description:
|
||||
name: async
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
boolean_selector:
|
||||
@ -14,7 +14,7 @@ packages:
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
characters:
|
||||
@ -22,7 +22,7 @@ packages:
|
||||
description:
|
||||
name: characters
|
||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
clock:
|
||||
@ -30,7 +30,7 @@ packages:
|
||||
description:
|
||||
name: clock
|
||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
collection:
|
||||
@ -38,7 +38,7 @@ packages:
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
cupertino_icons:
|
||||
@ -46,7 +46,7 @@ packages:
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
fake_async:
|
||||
@ -54,7 +54,7 @@ packages:
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
ffi:
|
||||
@ -62,7 +62,7 @@ packages:
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
file:
|
||||
@ -70,7 +70,7 @@ packages:
|
||||
description:
|
||||
name: file
|
||||
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
flutter:
|
||||
@ -88,7 +88,7 @@ packages:
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
flutter_test:
|
||||
@ -111,7 +111,7 @@ packages:
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.0"
|
||||
leak_tracker_flutter_testing:
|
||||
@ -119,7 +119,7 @@ packages:
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
leak_tracker_testing:
|
||||
@ -127,7 +127,7 @@ packages:
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
lints:
|
||||
@ -135,7 +135,7 @@ packages:
|
||||
description:
|
||||
name: lints
|
||||
sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
matcher:
|
||||
@ -143,7 +143,7 @@ packages:
|
||||
description:
|
||||
name: matcher
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.16+1"
|
||||
material_color_utilities:
|
||||
@ -151,7 +151,7 @@ packages:
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.0"
|
||||
meta:
|
||||
@ -159,7 +159,7 @@ packages:
|
||||
description:
|
||||
name: meta
|
||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.0"
|
||||
path:
|
||||
@ -167,7 +167,7 @@ packages:
|
||||
description:
|
||||
name: path
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
path_provider:
|
||||
@ -175,7 +175,7 @@ packages:
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
path_provider_android:
|
||||
@ -183,7 +183,7 @@ packages:
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.4"
|
||||
path_provider_foundation:
|
||||
@ -191,7 +191,7 @@ packages:
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
path_provider_linux:
|
||||
@ -199,7 +199,7 @@ packages:
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
@ -207,7 +207,7 @@ packages:
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
@ -215,7 +215,7 @@ packages:
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
permission_handler:
|
||||
@ -223,7 +223,7 @@ packages:
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.4.5"
|
||||
permission_handler_android:
|
||||
@ -231,7 +231,7 @@ packages:
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.3.6"
|
||||
permission_handler_apple:
|
||||
@ -239,7 +239,7 @@ packages:
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.1.4"
|
||||
permission_handler_platform_interface:
|
||||
@ -247,7 +247,7 @@ packages:
|
||||
description:
|
||||
name: permission_handler_platform_interface
|
||||
sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.12.0"
|
||||
permission_handler_windows:
|
||||
@ -255,7 +255,7 @@ packages:
|
||||
description:
|
||||
name: permission_handler_windows
|
||||
sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
platform:
|
||||
@ -263,7 +263,7 @@ packages:
|
||||
description:
|
||||
name: platform
|
||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
plugin_platform_interface:
|
||||
@ -271,7 +271,7 @@ packages:
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
process:
|
||||
@ -279,7 +279,7 @@ packages:
|
||||
description:
|
||||
name: process
|
||||
sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
sky_engine:
|
||||
@ -292,7 +292,7 @@ packages:
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
stack_trace:
|
||||
@ -300,7 +300,7 @@ packages:
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
stream_channel:
|
||||
@ -308,7 +308,7 @@ packages:
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
string_scanner:
|
||||
@ -316,7 +316,7 @@ packages:
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
sync_http:
|
||||
@ -324,7 +324,7 @@ packages:
|
||||
description:
|
||||
name: sync_http
|
||||
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
term_glyph:
|
||||
@ -332,7 +332,7 @@ packages:
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
test_api:
|
||||
@ -340,7 +340,7 @@ packages:
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.1"
|
||||
vector_math:
|
||||
@ -348,7 +348,7 @@ packages:
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
video_decode_plugin:
|
||||
@ -363,7 +363,7 @@ packages:
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.0"
|
||||
webdriver:
|
||||
@ -371,7 +371,7 @@ packages:
|
||||
description:
|
||||
name: webdriver
|
||||
sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
xdg_directories:
|
||||
@ -379,7 +379,7 @@ packages:
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
sdks:
|
||||
|
||||
@ -1,19 +1,209 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
|
||||
public class VideoDecodePlugin: NSObject, FlutterPlugin {
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let channel = FlutterMethodChannel(name: "video_decode_plugin", binaryMessenger: registrar.messenger())
|
||||
let instance = VideoDecodePlugin()
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
}
|
||||
public class VideoDecodePlugin: NSObject, FlutterPlugin, FlutterTexture {
|
||||
private var channel: FlutterMethodChannel?
|
||||
private var registrar: FlutterPluginRegistrar?
|
||||
private var decoder: VideoDecoder?
|
||||
private var textureId: Int64?
|
||||
private var textureRegistry: FlutterTextureRegistry?
|
||||
private var latestPixelBuffer: CVPixelBuffer?
|
||||
private let textureQueue = DispatchQueue(label: "video_decode_plugin.texture.queue")
|
||||
private var cachedSps: Data?
|
||||
private var cachedPps: Data?
|
||||
private var hasNotifiedFlutter = false
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
case "getPlatformVersion":
|
||||
result("iOS " + UIDevice.current.systemVersion)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let channel = FlutterMethodChannel(name: "video_decode_plugin", binaryMessenger: registrar.messenger())
|
||||
let instance = VideoDecodePlugin()
|
||||
instance.channel = channel
|
||||
instance.registrar = registrar
|
||||
instance.textureRegistry = registrar.textures()
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
}
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
case "initDecoder":
|
||||
handleInitDecoder(call: call, result: result)
|
||||
case "decodeFrame":
|
||||
handleDecodeFrame(call: call, result: result)
|
||||
case "releaseDecoder":
|
||||
handleReleaseDecoder(call: call, result: result)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化解码器
|
||||
private func handleInitDecoder(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
guard let args = call.arguments as? [String: Any],
|
||||
let width = args["width"] as? Int,
|
||||
let height = args["height"] as? Int,
|
||||
let codecType = args["codecType"] as? String else {
|
||||
result(FlutterError(code: "INVALID_ARGS", message: "参数错误", details: nil))
|
||||
return
|
||||
}
|
||||
// 释放旧解码器和纹理
|
||||
decoder?.release()
|
||||
decoder = nil
|
||||
if let tid = textureId {
|
||||
textureRegistry?.unregisterTexture(tid)
|
||||
textureId = nil
|
||||
}
|
||||
// 注册Flutter纹理
|
||||
guard let registry = textureRegistry else {
|
||||
result(FlutterError(code: "NO_TEXTURE_REGISTRY", message: "无法获取纹理注册表", details: nil))
|
||||
return
|
||||
}
|
||||
let textureId = registry.register(self)
|
||||
self.textureId = textureId
|
||||
// 创建解码器
|
||||
let decoder = VideoDecoder(width: width, height: height, codecType: codecType)
|
||||
self.decoder = decoder
|
||||
decoder.onFrameDecoded = { [weak self] pixelBuffer, _ in
|
||||
guard let self = self else { return }
|
||||
self.textureQueue.async {
|
||||
self.latestPixelBuffer = pixelBuffer
|
||||
self.textureRegistry?.textureFrameAvailable(self.textureId ?? 0)
|
||||
if !self.hasNotifiedFlutter {
|
||||
self.hasNotifiedFlutter = true
|
||||
DispatchQueue.main.async {
|
||||
self.channel?.invokeMethod("onFrameRendered", arguments: ["textureId": self.textureId ?? 0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
print("[VideoDecodePlugin] 解码器初始化成功,textureId=\(textureId)")
|
||||
result(textureId)
|
||||
}
|
||||
|
||||
/// 新增:去除NALU起始码的工具方法(增强日志与健壮性)
|
||||
private func stripStartCode(_ data: Data) -> Data {
|
||||
let originalLen = data.count
|
||||
let naluType: UInt8 = {
|
||||
if data.count > 4 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x00 && data[3] == 0x01 {
|
||||
return data[4] & 0x1F
|
||||
} else if data.count > 3 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01 {
|
||||
return data[3] & 0x1F
|
||||
}
|
||||
return 0
|
||||
}()
|
||||
var stripped: Data = data
|
||||
if data.count > 4 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x00 && data[3] == 0x01 {
|
||||
stripped = data.subdata(in: 4..<data.count)
|
||||
} else if data.count > 3 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01 {
|
||||
stripped = data.subdata(in: 3..<data.count)
|
||||
}
|
||||
let strippedLen = stripped.count
|
||||
let strippedType: UInt8 = stripped.count > 0 ? (stripped[0] & 0x1F) : 0
|
||||
if strippedLen < 3 || (strippedType != 7 && strippedType != 8) {
|
||||
print("[VideoDecodePlugin][警告] strip后NALU长度或类型异常,type=", strippedType, "len=", strippedLen)
|
||||
}
|
||||
// 只在异常时输出警告,不再输出详细内容
|
||||
return stripped
|
||||
}
|
||||
|
||||
/// 解码视频帧
|
||||
private func handleDecodeFrame(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
guard let args = call.arguments as? [String: Any],
|
||||
let frameData = args["frameData"] as? FlutterStandardTypedData,
|
||||
let frameType = args["frameType"] as? Int,
|
||||
let timestamp = args["timestamp"] as? Int,
|
||||
let frameSeq = args["frameSeq"] as? Int else {
|
||||
result(FlutterError(code: "INVALID_ARGS", message: "参数错误", details: nil))
|
||||
return
|
||||
}
|
||||
let refIFrameSeq = args["refIFrameSeq"] as? Int
|
||||
let data = frameData.data
|
||||
|
||||
// 解析NALU类型
|
||||
let naluType: UInt8 = {
|
||||
if data.count > 4 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x00 && data[3] == 0x01 {
|
||||
return data[4] & 0x1F
|
||||
} else if data.count > 3 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01 {
|
||||
return data[3] & 0x1F
|
||||
}
|
||||
return 0
|
||||
}()
|
||||
|
||||
print("[VideoDecodePlugin][调试] handleDecodeFrame: frameType=\(frameType), naluType=\(naluType), cachedSpsLen=\(cachedSps?.count ?? 0), cachedPpsLen=\(cachedPps?.count ?? 0)")
|
||||
|
||||
// 缓存SPS/PPS(去除起始码)并立即尝试初始化解码器
|
||||
if naluType == 7 { // SPS
|
||||
// 只缓存,不再输出详细日志
|
||||
cachedSps = stripStartCode(data)
|
||||
result(true)
|
||||
return
|
||||
} else if naluType == 8 { // PPS
|
||||
// 只缓存,不再输出详细日志
|
||||
cachedPps = stripStartCode(data)
|
||||
result(true)
|
||||
return
|
||||
} else if naluType == 5 { // IDR/I帧
|
||||
// 先提取第一个合法NALU,直接推送
|
||||
let firstNalu = extractFirstValidNalu(data)
|
||||
if firstNalu.isEmpty { return }
|
||||
print("[VideoDecodePlugin] 发送I帧, 长度: \(firstNalu.count), 头部: \(firstNalu.prefix(8).map { String(format: "%02X", $0) }.joined(separator: " ")), cachedSps长度: \(cachedSps?.count ?? 0), cachedPps长度: \(cachedPps?.count ?? 0)")
|
||||
decoder?.decodeFrame(frameData: firstNalu, frameType: frameType, timestamp: Int64(timestamp), frameSeq: frameSeq, refIFrameSeq: refIFrameSeq, sps: cachedSps, pps: cachedPps)
|
||||
} else {
|
||||
// 先提取第一个合法NALU,直接推送
|
||||
let firstNalu = extractFirstValidNalu(data)
|
||||
if firstNalu.isEmpty { return }
|
||||
print("[VideoDecodePlugin] 发送P/B帧, 长度: \(firstNalu.count), 头部: \(firstNalu.prefix(8).map { String(format: "%02X", $0) }.joined(separator: " "))")
|
||||
decoder?.decodeFrame(frameData: firstNalu, frameType: frameType, timestamp: Int64(timestamp), frameSeq: frameSeq, refIFrameSeq: refIFrameSeq)
|
||||
}
|
||||
result(true)
|
||||
}
|
||||
|
||||
/// 释放解码器资源
|
||||
private func handleReleaseDecoder(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
decoder?.release()
|
||||
decoder = nil
|
||||
if let tid = textureId {
|
||||
textureRegistry?.unregisterTexture(tid)
|
||||
textureId = nil
|
||||
}
|
||||
latestPixelBuffer = nil
|
||||
hasNotifiedFlutter = false
|
||||
print("[VideoDecodePlugin] 解码器和纹理已释放")
|
||||
result(true)
|
||||
}
|
||||
|
||||
// MARK: - FlutterTexture协议实现
|
||||
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
|
||||
var pixelBuffer: CVPixelBuffer?
|
||||
textureQueue.sync {
|
||||
pixelBuffer = self.latestPixelBuffer
|
||||
}
|
||||
if let pb = pixelBuffer {
|
||||
return Unmanaged.passRetained(pb)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 新增side_data检测工具
|
||||
private func checkNaluForSideData(_ nalu: Data, naluType: UInt8) -> Bool {
|
||||
if (naluType == 5 && nalu.count > 10000) || (naluType != 7 && naluType != 8 && nalu.count > 10000) {
|
||||
print("[VideoDecodePlugin][警告] NALU长度异常,可能包含side_data,type=\(naluType),len=\(nalu.count)")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 新增:提取第一个合法NALU工具
|
||||
private func extractFirstValidNalu(_ nalu: Data) -> Data {
|
||||
guard let start = nalu.range(of: Data([0x00, 0x00, 0x00, 0x01]))?.lowerBound else {
|
||||
print("[VideoDecodePlugin][警告] NALU无AnnexB起始码,丢弃该帧")
|
||||
return Data()
|
||||
}
|
||||
let searchRange = (start+4)..<nalu.count
|
||||
if let next = nalu[searchRange].range(of: Data([0x00, 0x00, 0x00, 0x01]))?.lowerBound {
|
||||
let end = searchRange.lowerBound + next
|
||||
return nalu[start..<end]
|
||||
} else {
|
||||
return nalu[start..<nalu.count]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
334
ios/Classes/VideoDecoder.swift
Normal file
334
ios/Classes/VideoDecoder.swift
Normal file
@ -0,0 +1,334 @@
|
||||
import Foundation
|
||||
import VideoToolbox
|
||||
import AVFoundation
|
||||
|
||||
/// 视频解码器,基于VideoToolbox实现H264/H265硬件解码,输出CVPixelBuffer
|
||||
class VideoDecoder {
|
||||
enum CodecType: String {
|
||||
case h264 = "h264"
|
||||
case h265 = "h265"
|
||||
var codecType: CMVideoCodecType {
|
||||
switch self {
|
||||
case .h264: return kCMVideoCodecType_H264
|
||||
case .h265: return kCMVideoCodecType_HEVC
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var decompressionSession: VTDecompressionSession?
|
||||
private var formatDesc: CMVideoFormatDescription?
|
||||
private let width: Int
|
||||
private let height: Int
|
||||
private let codecType: CodecType
|
||||
private let decodeQueue = DispatchQueue(label: "video_decode_plugin.decode.queue")
|
||||
private var isSessionReady = false
|
||||
private var lastIFrameSeq: Int?
|
||||
private var frameSeqSet = Set<Int>()
|
||||
private let maxAllowedDelayMs: Int = 350
|
||||
private var timestampBaseMs: Int64?
|
||||
private var firstFrameRelativeTimestamp: Int64?
|
||||
|
||||
// ===== 新增:缓冲区与自适应帧率相关成员 =====
|
||||
private let inputQueue = DispatchQueue(label: "video_decode_plugin.input.queue", attributes: .concurrent)
|
||||
private var inputBuffer: [(frameData: Data, frameType: Int, timestamp: Int64, frameSeq: Int, refIFrameSeq: Int?, sps: Data?, pps: Data?)] = []
|
||||
private let inputBufferSemaphore = DispatchSemaphore(value: 1)
|
||||
private let inputBufferMaxCount = 30
|
||||
|
||||
private let outputQueue = DispatchQueue(label: "video_decode_plugin.output.queue", attributes: .concurrent)
|
||||
private var outputBuffer: [(pixelBuffer: CVPixelBuffer, timestamp: Int64)] = []
|
||||
private let outputBufferSemaphore = DispatchSemaphore(value: 1)
|
||||
private let outputBufferMaxCount = 20
|
||||
|
||||
private var renderThread: Thread?
|
||||
private var renderThreadRunning = false
|
||||
private var hasNotifiedFlutter = false
|
||||
|
||||
// 帧率自适应参数
|
||||
private var renderFps: Int = 15
|
||||
private var smoothedFps: Double = 15.0
|
||||
private let alpha: Double = 0.2
|
||||
private let minFps: Double = 8.0
|
||||
private let maxFps: Double = 30.0
|
||||
private let maxStep: Double = 2.0
|
||||
private var renderedTimestamps: [Int64] = [] // ms
|
||||
private let renderedTimestampsMaxCount = 20
|
||||
private var renderedFrameCount = 0
|
||||
private let fpsAdjustInterval = 10
|
||||
|
||||
/// 解码回调
|
||||
var onFrameDecoded: ((CVPixelBuffer, Int64) -> Void)? = { _, _ in }
|
||||
|
||||
/// 初始化解码器
|
||||
init(width: Int, height: Int, codecType: String) {
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.codecType = CodecType(rawValue: codecType.lowercased()) ?? .h264
|
||||
startRenderThread()
|
||||
}
|
||||
|
||||
// ===== 新增:输入缓冲区入队方法 =====
|
||||
private func enqueueInput(_ item: (Data, Int, Int64, Int, Int?, Data?, Data?)) {
|
||||
inputQueue.async(flags: .barrier) {
|
||||
if self.inputBuffer.count >= self.inputBufferMaxCount {
|
||||
self.inputBuffer.removeFirst()
|
||||
}
|
||||
self.inputBuffer.append(item)
|
||||
}
|
||||
}
|
||||
// ===== 新增:输入缓冲区出队方法 =====
|
||||
private func dequeueInput() -> (Data, Int, Int64, Int, Int?, Data?, Data?)? {
|
||||
var item: (Data, Int, Int64, Int, Int?, Data?, Data?)?
|
||||
inputQueue.sync {
|
||||
if !self.inputBuffer.isEmpty {
|
||||
item = self.inputBuffer.removeFirst()
|
||||
}
|
||||
}
|
||||
return item
|
||||
}
|
||||
// ===== 新增:输出缓冲区入队方法 =====
|
||||
private func enqueueOutput(_ item: (CVPixelBuffer, Int64)) {
|
||||
outputQueue.async(flags: .barrier) {
|
||||
if self.outputBuffer.count >= self.outputBufferMaxCount {
|
||||
self.outputBuffer.removeFirst()
|
||||
}
|
||||
self.outputBuffer.append(item)
|
||||
}
|
||||
}
|
||||
// ===== 新增:输出缓冲区出队方法 =====
|
||||
private func dequeueOutput() -> (CVPixelBuffer, Int64)? {
|
||||
var item: (CVPixelBuffer, Int64)?
|
||||
outputQueue.sync {
|
||||
if !self.outputBuffer.isEmpty {
|
||||
item = self.outputBuffer.removeFirst()
|
||||
}
|
||||
}
|
||||
return item
|
||||
}
|
||||
// ===== 新增:渲染线程启动与停止 =====
|
||||
private func startRenderThread() {
|
||||
renderThreadRunning = true
|
||||
renderThread = Thread { [weak self] in
|
||||
guard let self = self else { return }
|
||||
while self.renderThreadRunning {
|
||||
let frameIntervalMs = Int(1000.0 / self.smoothedFps)
|
||||
let loopStart = Date().timeIntervalSince1970 * 1000.0
|
||||
if let (pixelBuffer, timestamp) = self.dequeueOutput() {
|
||||
// 渲染到Flutter纹理
|
||||
DispatchQueue.main.async {
|
||||
self.onFrameDecoded?(pixelBuffer, timestamp)
|
||||
}
|
||||
// 只在首次渲染时回调Flutter
|
||||
if !self.hasNotifiedFlutter {
|
||||
self.hasNotifiedFlutter = true
|
||||
// 由外部插件层负责onFrameRendered回调
|
||||
}
|
||||
// 帧率统计
|
||||
self.renderedTimestamps.append(Int64(Date().timeIntervalSince1970 * 1000))
|
||||
if self.renderedTimestamps.count > self.renderedTimestampsMaxCount {
|
||||
self.renderedTimestamps.removeFirst()
|
||||
}
|
||||
self.renderedFrameCount += 1
|
||||
if self.renderedFrameCount % self.fpsAdjustInterval == 0 {
|
||||
let measuredFps = self.calculateDecodeFps()
|
||||
let newFps = self.updateSmoothedFps(measuredFps)
|
||||
self.renderFps = newFps
|
||||
}
|
||||
}
|
||||
// 控制渲染节奏
|
||||
let loopCost = Int(Date().timeIntervalSince1970 * 1000.0 - loopStart)
|
||||
let sleepMs = frameIntervalMs - loopCost
|
||||
if sleepMs > 0 {
|
||||
Thread.sleep(forTimeInterval: Double(sleepMs) / 1000.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
renderThread?.start()
|
||||
}
|
||||
private func stopRenderThread() {
|
||||
renderThreadRunning = false
|
||||
renderThread?.cancel()
|
||||
renderThread = nil
|
||||
}
|
||||
// ===== 新增:帧率统计与EMA算法 =====
|
||||
private func calculateDecodeFps() -> Double {
|
||||
guard renderedTimestamps.count >= 2 else { return smoothedFps }
|
||||
let first = renderedTimestamps.first!
|
||||
let last = renderedTimestamps.last!
|
||||
let frameCount = renderedTimestamps.count - 1
|
||||
let durationMs = max(last - first, 1)
|
||||
return Double(frameCount) * 1000.0 / Double(durationMs)
|
||||
}
|
||||
private func updateSmoothedFps(_ measuredFps: Double) -> Int {
|
||||
let safeFps = min(max(measuredFps, minFps), maxFps)
|
||||
let targetFps = alpha * safeFps + (1 - alpha) * smoothedFps
|
||||
let delta = targetFps - smoothedFps
|
||||
let step = min(max(delta, -maxStep), maxStep)
|
||||
smoothedFps = min(max(smoothedFps + step, minFps), maxFps)
|
||||
return Int(smoothedFps)
|
||||
}
|
||||
/// 初始化解码会话(首次收到I帧时调用)
|
||||
private func setupSession(sps: Data?, pps: Data?) -> Bool {
|
||||
// 释放旧会话
|
||||
if let session = decompressionSession {
|
||||
VTDecompressionSessionInvalidate(session)
|
||||
decompressionSession = nil
|
||||
}
|
||||
formatDesc = nil
|
||||
isSessionReady = false
|
||||
guard let sps = sps, let pps = pps else {
|
||||
print("[VideoDecoder] 缺少SPS/PPS,无法初始化解码会话")
|
||||
return false
|
||||
}
|
||||
// 校验SPS/PPS长度和类型
|
||||
let spsType: UInt8 = sps.count > 0 ? (sps[0] & 0x1F) : 0
|
||||
let ppsType: UInt8 = pps.count > 0 ? (pps[0] & 0x1F) : 0
|
||||
if sps.count < 3 || spsType != 7 {
|
||||
print("[VideoDecoder][错误] SPS内容异常,len=\(sps.count), type=\(spsType)")
|
||||
return false
|
||||
}
|
||||
if pps.count < 3 || ppsType != 8 {
|
||||
print("[VideoDecoder][错误] PPS内容异常,len=\(pps.count), type=\(ppsType)")
|
||||
return false
|
||||
}
|
||||
var success = false
|
||||
sps.withUnsafeBytes { spsPtr in
|
||||
pps.withUnsafeBytes { ppsPtr in
|
||||
let parameterSetPointers: [UnsafePointer<UInt8>] = [
|
||||
spsPtr.baseAddress!.assumingMemoryBound(to: UInt8.self),
|
||||
ppsPtr.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
]
|
||||
let parameterSetSizes: [Int] = [sps.count, pps.count]
|
||||
let status = CMVideoFormatDescriptionCreateFromH264ParameterSets(
|
||||
allocator: kCFAllocatorDefault,
|
||||
parameterSetCount: 2,
|
||||
parameterSetPointers: parameterSetPointers,
|
||||
parameterSetSizes: parameterSetSizes,
|
||||
nalUnitHeaderLength: 4,
|
||||
formatDescriptionOut: &formatDesc
|
||||
)
|
||||
if status != noErr {
|
||||
print("[VideoDecoder] 创建FormatDescription失败: \(status)")
|
||||
success = false
|
||||
} else {
|
||||
success = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !success { return false }
|
||||
var callback = VTDecompressionOutputCallbackRecord(
|
||||
decompressionOutputCallback: { (decompressionOutputRefCon, _, status, _, imageBuffer, pts, _) in
|
||||
let decoder = Unmanaged<VideoDecoder>.fromOpaque(decompressionOutputRefCon!).takeUnretainedValue()
|
||||
if status == noErr, let pixelBuffer = imageBuffer {
|
||||
// 入队到输出缓冲区,由渲染线程拉取
|
||||
decoder.enqueueOutput((pixelBuffer, Int64(pts.seconds * 1000)))
|
||||
} else {
|
||||
print("[VideoDecoder] 解码回调失败, status=\(status)")
|
||||
}
|
||||
},
|
||||
decompressionOutputRefCon: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
|
||||
)
|
||||
let attrs: [NSString: Any] = [
|
||||
kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
|
||||
kCVPixelBufferWidthKey: width,
|
||||
kCVPixelBufferHeightKey: height,
|
||||
kCVPixelBufferOpenGLESCompatibilityKey: true
|
||||
]
|
||||
let status2 = VTDecompressionSessionCreate(
|
||||
allocator: kCFAllocatorDefault,
|
||||
formatDescription: formatDesc!,
|
||||
decoderSpecification: nil,
|
||||
imageBufferAttributes: attrs as CFDictionary,
|
||||
outputCallback: &callback,
|
||||
decompressionSessionOut: &decompressionSession
|
||||
)
|
||||
if status2 != noErr {
|
||||
print("[VideoDecoder] 创建解码会话失败: \(status2)")
|
||||
return false
|
||||
}
|
||||
isSessionReady = true
|
||||
print("[VideoDecoder] 解码会话初始化成功")
|
||||
return true
|
||||
}
|
||||
|
||||
/// 解码一帧数据
|
||||
func decodeFrame(frameData: Data, frameType: Int, timestamp: Int64, frameSeq: Int, refIFrameSeq: Int?, sps: Data? = nil, pps: Data? = nil) {
|
||||
enqueueInput((frameData, frameType, timestamp, frameSeq, refIFrameSeq, sps, pps))
|
||||
// 解码线程异步处理
|
||||
decodeQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard let (frameData, frameType, timestamp, frameSeq, refIFrameSeq, sps, pps) = self.dequeueInput() else { return }
|
||||
if !self.isSessionReady, let sps = sps, let pps = pps {
|
||||
guard self.setupSession(sps: sps, pps: pps) else { return }
|
||||
}
|
||||
guard let session = self.decompressionSession else { return }
|
||||
guard frameData.count > 4 else { return }
|
||||
var avccData = frameData
|
||||
let naluLen = UInt32(frameData.count - 4).bigEndian
|
||||
if avccData.count >= 4 {
|
||||
avccData.replaceSubrange(0..<4, with: withUnsafeBytes(of: naluLen) { Data($0) })
|
||||
} else {
|
||||
return
|
||||
}
|
||||
var blockBuffer: CMBlockBuffer?
|
||||
let status = CMBlockBufferCreateWithMemoryBlock(
|
||||
allocator: kCFAllocatorDefault,
|
||||
memoryBlock: UnsafeMutableRawPointer(mutating: (avccData as NSData).bytes),
|
||||
blockLength: avccData.count,
|
||||
blockAllocator: kCFAllocatorNull,
|
||||
customBlockSource: nil,
|
||||
offsetToData: 0,
|
||||
dataLength: avccData.count,
|
||||
flags: 0,
|
||||
blockBufferOut: &blockBuffer
|
||||
)
|
||||
if status != kCMBlockBufferNoErr { return }
|
||||
var sampleBuffer: CMSampleBuffer?
|
||||
var timing = CMSampleTimingInfo(duration: .invalid, presentationTimeStamp: CMTime(value: timestamp, timescale: 1000), decodeTimeStamp: .invalid)
|
||||
let status2 = CMSampleBufferCreate(
|
||||
allocator: kCFAllocatorDefault,
|
||||
dataBuffer: blockBuffer,
|
||||
dataReady: true,
|
||||
makeDataReadyCallback: nil,
|
||||
refcon: nil,
|
||||
formatDescription: self.formatDesc,
|
||||
sampleCount: 1,
|
||||
sampleTimingEntryCount: 1,
|
||||
sampleTimingArray: &timing,
|
||||
sampleSizeEntryCount: 1,
|
||||
sampleSizeArray: [avccData.count],
|
||||
sampleBufferOut: &sampleBuffer
|
||||
)
|
||||
if status2 != noErr { return }
|
||||
let decodeFlags: VTDecodeFrameFlags = []
|
||||
var infoFlags = VTDecodeInfoFlags()
|
||||
let status3 = VTDecompressionSessionDecodeFrame(
|
||||
session,
|
||||
sampleBuffer: sampleBuffer!,
|
||||
flags: decodeFlags,
|
||||
frameRefcon: nil,
|
||||
infoFlagsOut: &infoFlags
|
||||
)
|
||||
if status3 != noErr {
|
||||
print("[VideoDecoder] 解码失败: \(status3)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 释放解码器资源
|
||||
func release() {
|
||||
stopRenderThread()
|
||||
decodeQueue.sync {
|
||||
if let session = decompressionSession {
|
||||
VTDecompressionSessionInvalidate(session)
|
||||
}
|
||||
decompressionSession = nil
|
||||
formatDesc = nil
|
||||
isSessionReady = false
|
||||
frameSeqSet.removeAll()
|
||||
lastIFrameSeq = nil
|
||||
}
|
||||
inputQueue.async(flags: .barrier) { self.inputBuffer.removeAll() }
|
||||
outputQueue.async(flags: .barrier) { self.outputBuffer.removeAll() }
|
||||
print("[VideoDecoder] 解码器已释放")
|
||||
}
|
||||
}
|
||||
@ -8,11 +8,11 @@ class FrameDependencyManager {
|
||||
final List<int> _iFrameSeqWindow = [];
|
||||
|
||||
/// 更新SPS缓存
|
||||
void updateSps(Uint8List sps) {
|
||||
void updateSps(Uint8List? sps) {
|
||||
_sps = sps;
|
||||
}
|
||||
/// 更新PPS缓存
|
||||
void updatePps(Uint8List pps) {
|
||||
void updatePps(Uint8List? pps) {
|
||||
_pps = pps;
|
||||
}
|
||||
Uint8List? get sps => _sps;
|
||||
|
||||
@ -43,6 +43,11 @@ class VideoDecodePlugin {
|
||||
|
||||
static int? _textureId;
|
||||
|
||||
/// 获取默认纹理ID
|
||||
static int? get textureId => _textureId;
|
||||
|
||||
static final _depManager = FrameDependencyManager();
|
||||
|
||||
/// onFrameRendered回调类型(解码并已开始渲染)
|
||||
static void Function(int textureId)? _onFrameRendered;
|
||||
|
||||
@ -98,6 +103,8 @@ class VideoDecodePlugin {
|
||||
'textureId': _textureId,
|
||||
});
|
||||
_textureId = null;
|
||||
_depManager.updateSps(null);
|
||||
_depManager.updatePps(null);
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
@ -111,11 +118,6 @@ class VideoDecodePlugin {
|
||||
return Platform.isAndroid || Platform.isIOS;
|
||||
}
|
||||
|
||||
/// 获取默认纹理ID
|
||||
static int? get textureId => _textureId;
|
||||
|
||||
static final _depManager = FrameDependencyManager();
|
||||
|
||||
///
|
||||
/// [frameData]:帧数据
|
||||
/// [frameType]:帧类型 0=I帧, 1=P帧
|
||||
@ -130,6 +132,35 @@ class VideoDecodePlugin {
|
||||
required int frameSeq,
|
||||
bool splitNalFromIFrame = false,
|
||||
int? refIFrameSeq, // P帧时可选
|
||||
}) async {
|
||||
if (Platform.isAndroid) {
|
||||
await _sendFrameAndroid(
|
||||
frameData: frameData,
|
||||
frameType: frameType,
|
||||
timestamp: timestamp,
|
||||
frameSeq: frameSeq,
|
||||
splitNalFromIFrame: splitNalFromIFrame,
|
||||
refIFrameSeq: refIFrameSeq,
|
||||
);
|
||||
} else if (Platform.isIOS) {
|
||||
await _sendFrameIOS(
|
||||
frameData: frameData,
|
||||
frameType: frameType,
|
||||
timestamp: timestamp,
|
||||
frameSeq: frameSeq,
|
||||
refIFrameSeq: refIFrameSeq,
|
||||
);
|
||||
}
|
||||
// 其他平台暂不支持
|
||||
}
|
||||
|
||||
static Future<void> _sendFrameAndroid({
|
||||
required List<int> frameData,
|
||||
required int frameType,
|
||||
required int timestamp,
|
||||
required int frameSeq,
|
||||
bool splitNalFromIFrame = false,
|
||||
int? refIFrameSeq,
|
||||
}) async {
|
||||
if (splitNalFromIFrame && frameType == 0) {
|
||||
// 优先使用缓存的SPS/PPS
|
||||
@ -220,4 +251,96 @@ class VideoDecodePlugin {
|
||||
// 若为I帧,更新依赖管理
|
||||
if (frameType == 0) _depManager.updateIFrameSeq(frameSeq);
|
||||
}
|
||||
|
||||
static Future<void> _sendFrameIOS({
|
||||
required List<int> frameData,
|
||||
required int frameType,
|
||||
required int timestamp,
|
||||
required int frameSeq,
|
||||
int? refIFrameSeq,
|
||||
}) async {
|
||||
// 仅P帧做AnnexB起始码检测和修正
|
||||
if (frameType == 1) {
|
||||
// 分割NALU并只保留type=1的NALU
|
||||
final nalus = NaluUtils.splitNalus(frameData);
|
||||
final pNalus = nalus.where((nalu) => nalu.type == 1).toList();
|
||||
if (pNalus.isEmpty) {
|
||||
print('[VideoDecodePlugin][警告] iOS端P帧未找到type=1的NALU,已丢弃');
|
||||
return;
|
||||
}
|
||||
// 拼接所有type=1的NALU
|
||||
final sendData = <int>[];
|
||||
for (final nalu in pNalus) {
|
||||
sendData.addAll(nalu.data);
|
||||
}
|
||||
// print(
|
||||
// '[VideoDecodePlugin][调试] iOS端P帧仅推送type=1 NALU, count=${pNalus.length}, 总长度=${sendData.length}');
|
||||
frameData = sendData;
|
||||
int startIndex = -1;
|
||||
for (int i = 0; i < frameData.length - 3; i++) {
|
||||
if (frameData[i] == 0x00 &&
|
||||
frameData[i + 1] == 0x00 &&
|
||||
frameData[i + 2] == 0x00 &&
|
||||
frameData[i + 3] == 0x01) {
|
||||
startIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (startIndex == -1) {
|
||||
print('[VideoDecodePlugin][警告] iOS端P帧仅type=1 NALU后无AnnexB起始码,已丢弃');
|
||||
return;
|
||||
}
|
||||
if (startIndex > 0) {
|
||||
frameData = frameData.sublist(startIndex);
|
||||
}
|
||||
}
|
||||
if (frameType == 0) {
|
||||
// I帧需分割NALU,严格按SPS->PPS->I帧顺序推送
|
||||
final nalus = NaluUtils.splitNalus(frameData);
|
||||
List<int>? sps, pps, idr;
|
||||
for (final nalu in nalus) {
|
||||
if (nalu.type == 7) sps = nalu.data;
|
||||
if (nalu.type == 8) pps = nalu.data;
|
||||
if (nalu.type == 5) idr = nalu.data;
|
||||
}
|
||||
if (sps != null) {
|
||||
await _decodeFrame(
|
||||
frameData: Uint8List.fromList(sps),
|
||||
frameType: 0,
|
||||
timestamp: timestamp,
|
||||
frameSeq: frameSeq,
|
||||
refIFrameSeq: frameSeq,
|
||||
);
|
||||
}
|
||||
if (pps != null) {
|
||||
await _decodeFrame(
|
||||
frameData: Uint8List.fromList(pps),
|
||||
frameType: 0,
|
||||
timestamp: timestamp,
|
||||
frameSeq: frameSeq,
|
||||
refIFrameSeq: frameSeq,
|
||||
);
|
||||
}
|
||||
if (idr != null) {
|
||||
await _decodeFrame(
|
||||
frameData: Uint8List.fromList(idr),
|
||||
frameType: 0,
|
||||
timestamp: timestamp,
|
||||
frameSeq: frameSeq,
|
||||
refIFrameSeq: frameSeq,
|
||||
);
|
||||
}
|
||||
_depManager.updateIFrameSeq(frameSeq);
|
||||
return;
|
||||
}
|
||||
// 其他类型帧直接推送
|
||||
await _decodeFrame(
|
||||
frameData: Uint8List.fromList(frameData),
|
||||
frameType: frameType,
|
||||
timestamp: timestamp,
|
||||
frameSeq: frameSeq,
|
||||
refIFrameSeq: refIFrameSeq,
|
||||
);
|
||||
if (frameType == 0) _depManager.updateIFrameSeq(frameSeq);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user