feat(video,ios): 接入 iOS 端 ffmpeg-kit 转码桥
- 新增 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 链接头文件编译期报错属预期
This commit is contained in:
parent
4726107e19
commit
52a3e9f06c
8
Assets/Plugins/iOS/FFmpegKit.meta
Normal file
8
Assets/Plugins/iOS/FFmpegKit.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a31fe5a5335a6a84592ed4c2b335e1f0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
52
Assets/Plugins/iOS/FFmpegKit/README.md
Normal file
52
Assets/Plugins/iOS/FFmpegKit/README.md
Normal file
@ -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 行为一致)
|
||||
7
Assets/Plugins/iOS/FFmpegKit/README.md.meta
Normal file
7
Assets/Plugins/iOS/FFmpegKit/README.md.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fee61c27619655488da70cf0a955125
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
65
Assets/Plugins/iOS/FFmpegKitIOS.mm
Normal file
65
Assets/Plugins/iOS/FFmpegKitIOS.mm
Normal file
@ -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 <ffmpegkit/FFmpegKit.h>
|
||||
#import <ffmpegkit/FFmpegKitConfig.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <signal.h>
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Assets/Plugins/iOS/FFmpegKitIOS.mm.meta
Normal file
33
Assets/Plugins/iOS/FFmpegKitIOS.mm.meta
Normal file
@ -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:
|
||||
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 版没有该编码器。
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>异步 .264 -> .mp4 重编码转码(iOS)</summary>
|
||||
public static Task<bool> 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<bool> ConvertH264ToMp4Async(string inputPath, string outputPath)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user