fix:增加ios解码实现

This commit is contained in:
liyi 2025-05-07 15:07:36 +08:00
parent ca95779043
commit e375c0aeb4
11 changed files with 921 additions and 88 deletions

View File

@ -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
View 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

View File

@ -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";

View File

@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@ -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>

View File

@ -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('刷新'),
),
],
),
),

View File

@ -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:

View File

@ -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_datatype=\(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]
}
}
}
}

View File

@ -0,0 +1,334 @@
import Foundation
import VideoToolbox
import AVFoundation
/// VideoToolboxH264/H265CVPixelBuffer
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] 解码器已释放")
}
}

View File

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

View File

@ -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=1NALU
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=1NALU
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帧需分割NALUSPS->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);
}
}