From 52a3e9f06c3d41a31b9193c067bc79e94279a464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9C=E8=99=9E=E6=B8=A0=E6=88=90=E2=80=9D?= <“yuqucheng2006@qq.com”> Date: Tue, 8 Sep 2026 14:10:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(video,ios):=20=E6=8E=A5=E5=85=A5=20iOS=20?= =?UTF-8?q?=E7=AB=AF=20ffmpeg-kit=20=E8=BD=AC=E7=A0=81=E6=A1=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Assets/Plugins/iOS/FFmpegKitIOS.mm: ObjC++ extern C 桥,调 ffmpeg-kit executeWithArguments 做 libx264 重编码 - FFmpegKitBridge.cs 新增 UNITY_IOS 分支,经 DllImport(__Internal) 后台线程调用,参数与 Android 一致 - 新增 Assets/Plugins/iOS/FFmpegKit/README: 说明需放入的 ffmpegkit.xcframework 及构建来源 - 注: xcframework 需 macOS 自编译(arthenica ffmpeg-kit ios.sh --full --enable-gpl -x),放好前 .mm 链接头文件编译期报错属预期 --- Assets/Plugins/iOS/FFmpegKit.meta | 8 +++ Assets/Plugins/iOS/FFmpegKit/README.md | 52 ++++++++++++++ Assets/Plugins/iOS/FFmpegKit/README.md.meta | 7 ++ Assets/Plugins/iOS/FFmpegKitIOS.mm | 65 +++++++++++++++++ Assets/Plugins/iOS/FFmpegKitIOS.mm.meta | 33 +++++++++ Assets/Scripts/Video/FFmpegKitBridge.cs | 80 +++++++++++++++++++-- 6 files changed, 241 insertions(+), 4 deletions(-) create mode 100644 Assets/Plugins/iOS/FFmpegKit.meta create mode 100644 Assets/Plugins/iOS/FFmpegKit/README.md create mode 100644 Assets/Plugins/iOS/FFmpegKit/README.md.meta create mode 100644 Assets/Plugins/iOS/FFmpegKitIOS.mm create mode 100644 Assets/Plugins/iOS/FFmpegKitIOS.mm.meta diff --git a/Assets/Plugins/iOS/FFmpegKit.meta b/Assets/Plugins/iOS/FFmpegKit.meta new file mode 100644 index 0000000..5a76835 --- /dev/null +++ b/Assets/Plugins/iOS/FFmpegKit.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a31fe5a5335a6a84592ed4c2b335e1f0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/iOS/FFmpegKit/README.md b/Assets/Plugins/iOS/FFmpegKit/README.md new file mode 100644 index 0000000..e525797 --- /dev/null +++ b/Assets/Plugins/iOS/FFmpegKit/README.md @@ -0,0 +1,52 @@ +# iOS ffmpeg-kit 依赖放置说明 + +iOS 端转码复用 Android 同一套转码逻辑(libx264 重编码修复坏 PPS 裸流)。 +与 Android 的 `.so` 不同,iOS 用 ffmpeg-kit 的 **`.xcframework`**(内含 `ffmpegkit.framework`), +放在本目录后 Unity 会在构建 Xcode 工程时**自动链接**。 + +## 需要放入的文件 + +把 ffmpeg-kit iOS **full-gpl** 的 xcframework(含 libx264)解压后放进本目录,例如: + +``` +Assets/Plugins/iOS/FFmpegKit/ + └── ffmpegkit.xcframework/ + ├── Info.plist + ├── ios-arm64_armv7/ (真机切片) + │ └── ffmpegkit.framework/ + └── ios-arm64_i386_x86_64-simulator/ (模拟器切片,可选) + └── ffmpegkit.framework/ +``` + +(不同版本/项目解压出的骨架可能略有不同,只要有一个 `ffmpegkit.xcframework` 即可。) + +## 版本要求 + +与 Android 端保持一致,避免转码行为差异: + +- 来源: `dev.ffmpegkit-maintained:ffmpeg-kit-full-gpl:6.0.3`(FFmpeg **n6.1.6**,含 libx264) +- 必须是 **full-gpl**(含有)的 iOS 包;free/min 等版没有 libx264,转码会失败。 +- 若拿不到完全同 6.0.3 的 iOS 包,取同源(dev.ffmpegkit-maintained)最新 full-gpl 的 iOS + 包也可,但需确认 ffmpeg 仍能 `-c:v libx264`。 + +## 获取方式(macOS 上操作) + +1. 下载 ffmpeg-kit 维护版发布的 iOS full-gpl xcframework 压缩包 + (对应 Android AAR 那个 `ffmpeg-kit-full-gpl-6.0.3.aar` 的同批次 iOS 产物)。 +2. 解压得到 `ffmpegkit.xcframework`,拷入本目录。 +3. Unity 重新 import(会自动生成 `.meta`)。 + +> 注:ffmpeg-kit 的 Apple 产物需在 macOS + Xcode 下构建/发布,Windows 无法直接获得, +> 故本目录仅空占位 + 说明,实际 xcframework 需你在 Mac 上获取后放入。 + +## 调用链 + +- C# 桥: `Assets/Scripts/Video/FFmpegKitBridge.cs` 的 `#elif UNITY_IOS && !UNITY_EDITOR` 分支 + - `[DllImport("__Internal")]` 调本目录原生符号 `FFmpegKitExecuteWithArguments` + - 真正实现是 `FFmpegKitIOS.mm`(在 `Assets/Plugins/iOS/`,同目录上级) +- 下载/播放服务: `Assets/Scripts/Video/VideoDownloadPlayService.cs`(平台无关,已兼容) + +## 验证 + +- 转码日志应出现 `[FFmpegKitBridge] args: ... -c:v libx264 ...` 与 `[FFmpegKitBridge] rc=0` +- 生成的 mp4 可正常播放、不卡帧(与 Android 行为一致) \ No newline at end of file diff --git a/Assets/Plugins/iOS/FFmpegKit/README.md.meta b/Assets/Plugins/iOS/FFmpegKit/README.md.meta new file mode 100644 index 0000000..3b23233 --- /dev/null +++ b/Assets/Plugins/iOS/FFmpegKit/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1fee61c27619655488da70cf0a955125 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/iOS/FFmpegKitIOS.mm b/Assets/Plugins/iOS/FFmpegKitIOS.mm new file mode 100644 index 0000000..b0655bc --- /dev/null +++ b/Assets/Plugins/iOS/FFmpegKitIOS.mm @@ -0,0 +1,65 @@ +// +// FFmpegKitIOS.mm +// iOS 端 .264 -> .mp4 转码桥(extern "C"),供 C# 经 [DllImport("__Internal")] 调用。 +// +// 原理: 调用 native ffmpeg-kit(ffmpegkit.framework)的 executeWithArguments,用 libx264 +// 重编码重新生成干净的 SPS/PPS + 时间戳,修复设备(OPPO/MTK 等)产出裸流坏 PPS 导致的 +// 解码卡死("non-existing PPS 0 referenced")。 +// +// 依赖: 需将 ffmpeg-kit iOS full-gpl 的 .xcframework(含 ffmpegkit.framework)放进 +// Assets/Plugins/iOS/FFmpegKit/(Unity 会在构建 Xcode 工程时自动链接)。 +// 版本需与本项目 Android 端一致: dev.ffmpegkit-maintained:ffmpeg-kit-full-gpl:6.0.3 +// (FFmpeg n6.1.6,含 libx264)。缺少该 framework 时本文件编译期即报错找不到头文件,属预期。 +// +#import +#import +#import +#import + +static BOOL ffmpegKitConfigured = NO; + +// 一次性配置:忽略 Unity/IL2CPP 运行时可能被 ffmpeg-kit 信号处理干扰的信号 +static void FFmpegKitConfigureOnce(void) { + if (ffmpegKitConfigured) return; + ffmpegKitConfigured = YES; + [FFmpegKitConfig ignoreSignal:SIGXCPU]; + [FFmpegKitConfig ignoreSignal:SIGPIPE]; +} + +extern "C" { + + /// 执行 ffmpeg 转码。 + /// @param argv 参数数组(C# string[] 会作为 UTF-8 char** 传入) + /// @param argc 参数个数 + /// @return 0=成功;否则为 ffmpeg 退出码;-1=参数或内部错误 + int FFmpegKitExecuteWithArguments(char **argv, int argc) { + @autoreleasepool { + FFmpegKitConfigureOnce(); + + if (argv == NULL || argc <= 0) { + NSLog(@"[FFmpegKitIOS] bad args argv=%p argc=%d", argv, argc); + return -1; + } + + NSMutableArray *arguments = [NSMutableArray arrayWithCapacity:(NSUInteger)argc]; + for (int i = 0; i < argc; i++) { + if (argv[i] == NULL) continue; + [arguments addObject:[NSString stringWithUTF8String:argv[i]]]; + } + + NSLog(@"[FFmpegKitIOS] execute: %@", [arguments componentsJoinedByString:@" "]); + + FFmpegSession *session = [FFmpegKit executeWithArguments:arguments]; + ReturnCode *returnCode = [session getReturnCode]; + + if ([ReturnCode isSuccess:returnCode]) { + return 0; + } + if (returnCode == nil) { + NSLog(@"[FFmpegKitIOS] returnCode nil, treat as failure."); + return -1; + } + return (int)[returnCode getValue]; + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/iOS/FFmpegKitIOS.mm.meta b/Assets/Plugins/iOS/FFmpegKitIOS.mm.meta new file mode 100644 index 0000000..6531ce1 --- /dev/null +++ b/Assets/Plugins/iOS/FFmpegKitIOS.mm.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: 8963e0eb06dd1fe418eeae84f183fd4a +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Video/FFmpegKitBridge.cs b/Assets/Scripts/Video/FFmpegKitBridge.cs index b2a26d8..e0b762e 100644 --- a/Assets/Scripts/Video/FFmpegKitBridge.cs +++ b/Assets/Scripts/Video/FFmpegKitBridge.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Kill.Utils; using UnityEngine; @@ -7,12 +8,17 @@ using UnityEngine; namespace Kill.Video { /// - /// ffmpeg-kit-maintained(维护版,群组 dev.ffmpegkit-maintained)C# 桥接 - /// 用 AndroidJNI 底层 API 直接调,绕开 AndroidJavaClass.CallStatic 的 Unity bug(Ljava.lang.Object) + /// ffmpeg-kit(维护版,群组 dev.ffmpegkit-maintained)C# 桥接。 /// .264 -> .mp4 用 libx264 重编码(full-gpl 版),补齐干净的 SPS/PPS + 时间戳, /// 供各手机(含 OPPO/MTK)解码器正常播放;纯 copy 会复制坏帧导致播放卡死。 - /// 注意:所有 JNI 调用必须在 Unity 主线程执行(IL2CPP 工作线程 JVM attach 不可靠,FindClass - /// 偶发失败);现依赖为 ffmpeg-kit-full-gpl(含 libx264),free 版没有该编码器。 + /// + /// 平台差异: + /// - Android: 用 AndroidJNI 底层 API 直接调,绕开 AndroidJavaClass.CallStatic 的 Unity + /// bug(Ljava.lang.Object);所有 JNI 必须在 Unity 主线程执行(IL2CPP 工作线程 JVM attach + /// 不可靠,FindClass 偶发失败),经 MainThreadDispatcher 派发。 + /// - iOS: 经 Assets/Plugins/iOS/FFmpegKitIOS.mm(extern "C")调 native ffmpeg-kit, + /// [DllImport("__Internal")] 直接在后台上线程执行,无 JNI 约束。 + /// 现依赖 ffmpeg-kit-full-gpl(含 libx264),free 版没有该编码器。 /// public static class FFmpegKitBridge { @@ -253,7 +259,73 @@ namespace Kill.Video } #endregion + #elif UNITY_IOS && !UNITY_EDITOR + // iOS: 经 Assets/Plugins/iOS/FFmpegKitIOS.mm(extern "C") 调 native ffmpeg-kit, + // 结构与 Android 相同,但无需 JNI/主线程约束,直接在后台线程执行不阻塞 UI。 + [DllImport("__Internal")] + private static extern int FFmpegKitExecuteWithArguments(string[] args, int argc); + + /// 异步 .264 -> .mp4 重编码转码(iOS) + public static Task ConvertH264ToMp4Async(string inputPath, string outputPath) + { + if (string.IsNullOrEmpty(inputPath) || !File.Exists(inputPath)) + return Task.FromResult(false); + if (File.Exists(outputPath)) + { + try { File.Delete(outputPath); } catch { } + } + + // 与 Android 完全一致的参数:libx264 重编码重新生成干净的 SPS/PPS + 时间戳, + // 修复坏 PPS 裸流导致的解码卡死("non-existing PPS 0 referenced") + string[] args = new string[] { + "-y", + "-loglevel", "quiet", + "-fflags", "+genpts", + "-i", inputPath, + "-c:v", "libx264", + "-preset", "ultrafast", + "-crf", "23", + "-pix_fmt", "yuv420p", + "-movflags", "+faststart", + outputPath + }; + Debug.Log("[FFmpegKitBridge] args: " + string.Join(" ", args)); + + return Task.Run(() => + { + try + { + int rc = FFmpegKitExecuteWithArguments(args, args.Length); + Debug.Log("[FFmpegKitBridge] rc=" + rc); + bool ok = rc == 0; + // 兜底:rc 非 0 但 mp4 已写出且 >0,(与 Android 一致的 signal 场景)按成功处理 + if (!ok) + { + try + { + if (File.Exists(outputPath)) + { + var fi = new FileInfo(outputPath); + if (fi.Length > 0) + { + Debug.LogWarning("[FFmpegKitBridge] ffmpeg rc=" + rc + " 但 mp4 已生成,按成功处理"); + ok = true; + } + } + } + catch { } + } + return ok; + } + catch (Exception e) + { + Debug.LogError("[FFmpegKitBridge] execute fail: " + e); + return false; + } + }); + } #else + // 编辑器 / 其他平台:不执行转码 public static Task ConvertH264ToMp4Async(string inputPath, string outputPath) { return Task.FromResult(false);