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_64arm64,不過應該沒人用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就是用來連接xcframeworkSwift用的,所以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次,才找到最安全簡單的方式說明,果然還是去雪山救狐狸,或吃隻醬板鴨來得實在啊…