FFMpeg跟iOS終於在一起了
其實在這個AI的時代,許多的程式碼越來越容易取得了,你寫的程式碼只要流出來,馬上就被AI學走,這也算是一種眾人的智慧吧?但本著大愛的精神,我還是不藏私啊,但在這類跨平台的功能,還是滿不容易找到的,因為只有build而己,要說它是程式,也不算是,但又一定用得到,像一些第三方登入的SDK,為了安全,還是會包成二進制檔案,但是呢,通常不是只包成.a檔,細節要自己處理,就是只有.framework檔,只能在實機上使用,所以呢,想自己包個能在模擬器上用的ffmpeg的念頭就產生了…
做一個長得像這樣的東西
作業環境
| 項目 | 版本 |
|---|---|
| macOS | Sequoia 26.3.1 |
| Xcode | 26.3 |
| yasm | 1.3.0 |
安裝Yasm
它是一個開源的 x86/AMD64 架構組譯器,是 NASM 的完全重寫版。它支援 16、32 和 64 位元程式,相容 NASM 與 GNU 組譯器語法,並能輸出多種物件格式(如 ELF, COFF, Mach-O)。
- 順道建立要下載FFMpeg原始碼的資料夾 -
FFMpeg_XCFramework
brew install yasm
mkdir FFMpeg_XCFramework
cd FFMpeg_XCFramework
下載v7.1版原始碼
- v7.1版的比較穩定,而v8.1還在更新中,所以選用v7版本的…
export Source_Dir="src"
git clone --branch n7.1 https://git.ffmpeg.org/ffmpeg.git ${Source_Dir}
設定環境變數
- 這些
變數是後面要使用到的… - 主要是設定要編成
模擬器版本的,還是實機版的… - 其中
Arch變數是CPU用的,所以有x86_64或arm64,不過應該沒人用x86_64來編了吧? - 然後
Target變數是設定給clang/clang++用的,這裡會用到給手機用arm64-apple-ios,或給模擬器用arm64-apple-ios-simulator… SDK變數就是要使用的SDK,給手機用的 -iphoneos,或是給模擬器用的 -iphonesimulator…- 而
Type變數也是給clang/clang++用的,給手機用的 -iphoneos,或是給模擬器用的 -ios-simulator… - 這個
SupportedPlatform變數是給xcframeword的說明檔 -Info.plist用的,告訴Xcode,哪個是給手機用的,哪個是給模擬器用的,這裡我們用iPhoneOS/iPhoneSimulator,當然也有其它的選項,只是目前用不到… - 不過因為這些變數只會在當前的
session有效,所以都要在同一個視窗內作業喲…
export Source_Dir="src"
export Arch="arm64"
export Target="arm64-apple-ios-simulator"
export SDK="iphonesimulator"
export Type="ios-simulator"
export MinVersion="16.0"
export SupportedPlatform="iPhoneSimulator"
export Framework_Path=${Arch}-${SDK}
export SYSROOT=$(xcrun --sdk ${SDK} --show-sdk-path)
export CC="xcrun -sdk ${SDK} clang -target ${Target}"
export CXX="xcrun -sdk ${SDK} clang++ -target ${Target}"
export CFLAGS="-arch ${Arch} -isysroot ${SYSROOT} -m${Type}-version-min=${MinVersion}"
export LDFLAGS="-arch ${Arch} -isysroot ${SYSROOT} -m${Type}-version-min=${MinVersion} -headerpad_max_install_names"
建立編譯資料夾
- 因為會編成兩個版本,所以要建立個別的資料夾,後面才能合併…
mkdir installed
mkdir build
mkdir build/${Framework_Path}
cd build/${Framework_Path}
設定編譯檔
- 這裡主要是使用
FFMpeg內建的編譯檔產生器來處理,不然檔案那麼多,要怎麼編啊? - 在這裡會要一點時間,因為要產生滿多的設定檔的…
../../${Source_Dir}/configure \
--prefix=../../installed/${Framework_Path} \
--disable-static \
--enable-shared \
--enable-cross-compile \
--target-os=darwin \
--arch=${Arch} \
--sysroot=${SYSROOT} \
--extra-cflags="-arch ${Arch} -m${Type}-version-min=${MinVersion} -fembed-bitcode" \
--extra-ldflags="-arch ${Arch} -m${Type}-version-min=${MinVersion}" \
--cc=${CC} \
--cxx=${CXX} \
--install-name-dir=@rpath \
--disable-audiotoolbox \
--disable-doc \
--disable-programs \
--disable-videotoolbox \
--enable-network
開始編譯
- 在這裡,我們開啟多線程編譯,然後把要安裝的檔,通通丟到
installed上… - 編譯完成,可以看到有7個主功能,分別是
libavcodec/libavdevice/libavfilter/libavformat/libavutil/libswresample/libswscale…
make -j$(sysctl -n hw.ncpu)
make install
cd ../..
產生framework
- 接下來就把編好的檔案,合成一個
framework,以便後面使用… - 在專案
root之下,建立一個create.sh的檔案…
#!/bin/bash
set -e
create_plist() {
local framework_name="$1"
local file_path="$2"
local info_plist="<?xml version="1.0" encoding="UTF-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>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${framework_name}</string>
<key>CFBundleIdentifier</key>
<string>org.ffmpeg.${framework_name}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${framework_name}</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>7.1</string>
<key>CFBundleVersion</key>
<string>7.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>MinimumOSVersion</key>
<string>${MinVersion}</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>${SupportedPlatform}</string>
</array>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>"
echo "$info_plist" > "$file_path"
}
create_framework() {
local framework_name="$1"
local ffmpeg_library_path="./installed"
local framework_dir="${ffmpeg_library_path}/${Framework_Path}/framework/${framework_name}.framework"
local dylib_src="${ffmpeg_library_path}/${Framework_Path}/lib/${framework_name}.dylib"
local dylib_dst="${framework_dir}/${framework_name}"
mkdir -p "$framework_dir"
cp "$dylib_src" "$dylib_dst"
create_plist "$framework_name" "${framework_dir}/Info.plist"
install_name_tool -id "@rpath/${framework_name}.framework/${framework_name}" "$dylib_dst"
otool -L "$dylib_dst" | grep "@rpath/.*\.dylib" | awk '{print $1}' | while read -r dependency; do
local dep_name=$(basename "$dependency" | sed 's/\..*//' | sed 's/^lib//')
local new_path="@rpath/lib${dep_name}.framework/lib${dep_name}"
install_name_tool -change "$dependency" "$new_path" "$dylib_dst"
done
}
for lib in libavcodec libavdevice libavfilter libavformat libavutil libswresample libswscale; do
create_framework "$lib"
done
for lib in libavcodec libavdevice libavfilter libavformat libavutil libswresample libswscale; do
mkdir -p installed/${Framework_Path}/framework/${lib}.framework/Headers
cp -r installed/${Framework_Path}/include/${lib}/* installed/${Framework_Path}/framework/${lib}.framework/Headers/
done
- 把上述的指令貼上後,轉成
可執行檔,然後執行它… - 其實,這樣子就可以給
xcode用了,但只能在模擬器上使用…
chmod +x create.sh
./create.sh
建立實機版的framework
- 讓我們開啟一個新的命令視窗,動作完全一樣,從頭再做一次,只是把參數換成一下給
實機用而己… - 這裡就不多說了,我們就
快轉啦,順序一樣是configure->make->create.sh,完全不用改,筆者都幫您設定好了…
export Source_Dir="src"
export Arch="arm64"
export Target="arm64-apple-ios"
export SDK="iphoneos"
export Type="iphoneos"
export SupportedPlatform="iPhoneOS"
export MinVersion="16.0"
export Framework_Path=${Arch}-${SDK}
export SYSROOT=$(xcrun --sdk ${SDK} --show-sdk-path)
export CC="xcrun -sdk ${SDK} clang -target ${Target}"
export CXX="xcrun -sdk ${SDK} clang++ -target ${Target}"
export CFLAGS="-arch ${Arch} -isysroot ${SYSROOT} -m${Type}-version-min=${MinVersion}"
export LDFLAGS="-arch ${Arch} -isysroot ${SYSROOT} -m${Type}-version-min=${MinVersion} -headerpad_max_install_names"
合併framework
-
編好之後,就可以在
installed資料夾,看到有arm64-iphoneos實機版的 +arm64-iphonesimulator模擬器版的framework…
-
接下來,我們進到
installed,建立一個叫combine.sh的執行檔,步驟跟create.sh一樣…
cd installed
- 執行
combine.sh就可以看到合併好的xcframework了,一共有7組…
#!/bin/bash
set -e
for lib in libavcodec libavdevice libavfilter libavformat libavutil libswresample libswscale; do
xcodebuild -create-xcframework \
-framework arm64-iphoneos/framework/${lib}.framework \
-framework arm64-iphonesimulator/framework/${lib}.framework \
-output xcframework/${lib}.xcframework
done
使用xcframework
- 我們先將7組
xcframework貼到新開的Xcode專案之上… - 然後新增
Cocoa Touch Class的檔案,使用Objective-C,名叫FFmpegWrapper…
Bridging-Header.h
- 這個
FFmpegWrapper.h就是用來連接xcframework給Swift用的,所以Objective-C不能亡啊… - 然後就會跑出一個需不需要
Bridging-Header.h的彈窗,就給它加入吧… - 在
FFmpegWrapper.h+FFmpegWrapper.m寫入對應的程式碼…
// FFmpegWrapper.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FFmpegWrapper : NSObject
+ (NSTimeInterval)getDurationOfVideoAtURL: (NSURL *)url;
@end
NS_ASSUME_NONNULL_END
- 以下的功能,主要是取得
影片的長度…
// FFmpegWrapper.m
#import "FFmpegWrapper.h"
#import <libavformat/avformat.h>
#import <libavutil/dict.h>
@implementation FFmpegWrapper
+ (NSTimeInterval)getDurationOfVideoAtURL: (NSURL *)url {
AVFormatContext *formatContext = NULL;
avformat_network_init();
int result = avformat_open_input(&formatContext, [url.path UTF8String], NULL, NULL);
if (result != 0) {
NSLog (@"Could not open video file: %s", av_err2str(result));
return 0;
}
result = avformat_find_stream_info(formatContext, NULL);
if (result < 0) {
NSLog (@"Could not retrieve stream info: %s", av_err2str(result));
avformat_close_input(&formatContext);
return 0;
}
NSTimeInterval durationInSeconds = (NSTimeInterval) formatContext-> duration / AV_TIME_BASE;
avformat_close_input(&formatContext);
avformat_network_deinit();
return durationInSeconds;
}
@end
- 最後把
FFmpegWrapper給它橋接起來就可以了…
// Example-Bridging-Header.h
#import "FFmpegWrapper.h"
結果
- 最後我們來試用看看吧,
影片就從這裡下載…
https://peach.blender.org/download/
- 看看有沒有正確取得影片長度…
- 如果build不成功的,可以直接到這裡下載筆者編譯好的…
import UIKit
final class ViewController: UIViewController {
private let filename: String = "BigBuckBunny.mp4"
override func viewDidLoad() {
super.viewDidLoad()
guard let url = Bundle.main.url(forResource: filename, withExtension: nil) else { return }
let time = FFmpegWrapper.getDurationOfVideo(at: url)
print(time)
}
}
範例程式碼下載
後記
其實,編譯這種大型程式碼,也不是件簡單事,為了寫這篇文章,也是很累人的,build了不下30次,才找到最安全簡單的方式說明,果然還是去雪山救狐狸,或吃隻醬板鴨來得實在啊…