iOSアプリからffmpegのAPIを使う。
【前提知識】iOSの共有ライブラリ(Fat Binary)の使い方。
-
Fat Binaryについて
OSX(x86_64), iPhoneシミュレータ(i386)、iPhone実機(arm7,arm64)はCPUアーキテクチャが違うので、 それぞれのバイナリが必要。 複数のアーキテクチャに対応したバイナリをFat Binaryという。
アーキテクチャはコンパイルオプション -arch で指定する。
$ gcc -arch [x86_64 | i386 | armv7 | arm64] ...
アーキテクチャによって使うSDK(ヘッダとライブラリ)が違うのでインクルードパスを変更する。 これには xcrun が便利で、オプション指定でコマンド実行時の環境変数 SDKROOT に SDKディレクトリが設定される。
$ xcrun -sdk [macosx | iphonesimulator | iphoneos] \ gcc -I$SDKROOT/usr/inclde -L$SDKROOT/usr/lib ...
実際の環境変数 SDKROOT の値は /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk のように非常に長い。
アーキテクチャごとのライブラリ(.a)を作り、これらを lipo コマンドで結合して Fat Binary にする。
$ lipo -create libmylib_x86_64.a libmylib_i386.a \ libmylib_armv7.a libmylib_arm64.a \ -output libmylib_fat.a
-
ライブラリのビルド例。
ソースは前回の mylib.h mylib.c main.c を使う。
$ xcrun -sdk macosx gcc -arch x86_64 -I$SDKROOT/usr/inclde -L$SDKROOT/usr/lib -c mylib.c -o mylib_x86_64.o -miphoneos-version-min=7.0 $ xcrun -sdk iphonesimulator gcc -arch i386 -I$SDKROOT/usr/inclde -L$SDKROOT/usr/lib -c mylib.c -o mylib_i386.o -miphoneos-version-min=7.0 $ xcrun -sdk iphoneos gcc -arch armv7 -I$SDKROOT/usr/inclde -L$SDKROOT/usr/lib -c mylib.c -o mylib_armv7.o -miphoneos-version-min=7.0 $ xcrun -sdk iphoneos gcc -arch arm64 -I$SDKROOT/usr/inclde -L$SDKROOT/usr/lib -c mylib.c -o mylib_arm64.o -miphoneos-version-min=7.0 ar -r libmylib_x86_64.a mylib_x86_64.o ar -r libmylib_i386.a mylib_i386.o ar -r libmylib_armv7.a mylib_armv7.o ar -r libmylib_arm64.a mylib_arm64.o lipo -create libmylib_x86_64.a libmylib_i386.a libmylib_armv7.a libmylib_arm64.a -output libmylib_fat.a
できた .o や .a の対応アーキテクチャは lipo で調べる。
$ lipo -info mylib_x86_64.o Non-fat file: mylib_x86_64.o is architecture: x86_64 $ lipo -info libmylib_fat.a Architectures in the fat file: libmylib_fat.a are: i386 armv7 x86_64 arm64
※コンパイルオプション -miphoneos-version-min=7.0 はXcode7でのリンクエラー対策。 詳細はこちら。
-
Xcodeから使う。
ヘッダ(.h)とライブラリ(.a)を PROJECT_DIR (.xcodeprojのある場所) の mylib/ に置いた場合。
-
サーチパスの設定。
Target → Build Settings → Search Paths
- Header Search Paths : $(PROJECT_DIR)/mylib
- Library Search Paths : $(PROJECT_DIR)/mylib
-
ライブラリの登録。
Target → Build Phases → Link Binary with Libraries → + → Add Other...
- $(PROJECT_DIR)/mylib から libmylib_fat.a を追加。
-
プログラム。
AppDelegate.m
#import "AppDelegate.h" #include <mylib.h> @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { puts(hello()); return YES; } @end
-
実行結果。(コンソール)
hello
-
サーチパスの設定。
ffmpegのiOS用APIの使い方。
-
ライブラリのビルド。
FFmpeg-iOS-build-script(GitHub) は素晴らしいスクリプトで、 ffmpegのソースダウンロードからinclude/lib生成まで全てを行ってくれる。
GitHubからスクリプトを適当な作業ディレクトリにダウンロードして、
$ build-ffmpeg.sh
これで ./FFmpeg-iOS/include と ./FFmpeg-iOS/lib ができる。 このとき依存ツール (yasm, brew, gas-preprocessor) は必要に応じてインストールされる。 けっこう時間がかかる。 MacBook Pro (Late 2013, Core i5 2.4GHz デュアルコア メモリ4GB) だと40分ぐらいかかる。
-
Xcodeから使う。
FFmpeg-iOS/ を PROJECT_DIR (.xcodeprojのある場所) に置いた場合。
-
サーチパスの設定。
Target → Build Settings → Search Paths
- Header Search Paths : $(PROJECT_DIR)/FFmpeg-iOS/include
- Library Search Paths : $(PROJECT_DIR)/FFmpeg-iOS/lib
-
ライブラリの登録。
Target → Build Phases → Link Binary with Libraries → +
-
Add Other...
$(PROJECT_DIR)/FFmpeg-iOS/lib から libavcodec, libavdevice, libavfilter, libavformat, libavutil, libswresample, libswscale を追加。
-
frameworkリスト
libz, libbz2, libiconv, VideoToolbox.framework, CoreMedia.framework を追加。
-
Add Other...
-
プログラム。
AppDelegate.m
#import "AppDelegate.h" #import "ViewController.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // ウィンドウの生成。 self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; // ビューコントローラの生成。 ViewController* viewCtl = [[ViewController alloc] init]; self.window.rootViewController = viewCtl; return YES; } @end
ViewController.m// 動画ファイルから1フレームごとにUIImageに格納し連続表示する。 // デコードは別スレッドで行い、1フレーム完了するたびにメインスレッドでコールバックする。 // (全てメインスレッドで実行すると、常にスレッドがビジーで表示が更新されない。) // だいたい30fpsになるよう、デコーダスレッド側でwaitを入れる。 #import "ViewController.h" #include <libavcodec/avcodec.h> #include <libavfilter/avfilter.h> #include <libavformat/avformat.h> #include <libavutil/imgutils.h> #include <libswscale/swscale.h> @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // イメージビューの作成。 UIImageView* imageView = [[UIImageView alloc] initWithFrame:self.view.bounds]; imageView.contentMode = UIViewContentModeScaleAspectFit; [self.view addSubview:imageView]; // 動画のデコードとコマ画像の表示。 [self parseVideoAndGotFrame:^(UIImage* image) { imageView.image = image; }]; } // 動画のデコードとコマ画像の生成。 - (void)parseVideoAndGotFrame:(void (^)(UIImage* image))gotFrame { // バックグラウンドでデコード処理。 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // リソース動画ファイルのパス取得。 NSURL* videoUrl = [[NSBundle mainBundle] URLForResource:@"a" withExtension:@"mp4"]; const char* inputFilename = [[videoUrl absoluteString] UTF8String]; // 初期化。 av_register_all(); // 動画ファイルのオープンとストリーム情報の取得。 AVFormatContext* pFormatCtx = NULL; if (avformat_open_input(&pFormatCtx, inputFilename, NULL, NULL) != 0) { printf("Error: avformat_open_input()\n"); return; } if (avformat_find_stream_info(pFormatCtx, NULL) < 0) { printf("Error: avformat_find_stream_info()\n"); return; } // Videoストリームを探す。 int videoStreamIndex = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); if (videoStreamIndex < 0) { printf("Error: av_find_best_stream() for video\n"); return; } // Videoコーデックの取得とオープン。 AVStream* pVideoStream = pFormatCtx->streams[videoStreamIndex]; AVCodecContext* pVideoCodecCtx = pVideoStream->codec; AVCodec* pVideoCodec = avcodec_find_decoder(pVideoCodecCtx->codec_id); if (pVideoCodec == 0) { printf("Error: avcodec_find_decoder()\n"); return; } if (avcodec_open2(pVideoCodecCtx, pVideoCodec, NULL) < 0) { printf("Error: avcodec_open2() for video\n"); return; } float videoTimeBase = av_q2d(pFormatCtx->streams[videoStreamIndex]->time_base); float videoFrameRate = av_q2d(pVideoStream->r_frame_rate); printf("video stream: #%d %s %f fps\n", videoStreamIndex, pVideoCodec->name, videoFrameRate); // Audioストリームを探す。 int audioStreamIndex = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0); if (audioStreamIndex < 0) { printf("Error: av_find_best_stream() for audio\n"); return; } // Audioコーデックの取得とオープン。 AVCodecContext* pAudioCodecCtx = pFormatCtx->streams[audioStreamIndex]->codec; AVCodec* pAudioCodec = avcodec_find_decoder(pAudioCodecCtx->codec_id); if (pAudioCodec == 0) { printf("Error: avcodec_find_decoder() audio\n"); return; } if (avcodec_open2(pAudioCodecCtx, pAudioCodec, NULL) < 0) { printf("Error: avcodec_open2() for audio\n"); return; } float audioTimeBase = av_q2d(pFormatCtx->streams[audioStreamIndex]->time_base); int audioSampleRate = pAudioCodecCtx->sample_rate; printf("audio stream: #%d %s %d Hz\n", audioStreamIndex, pAudioCodec->name, audioSampleRate); // Video/Audio読み込み用フレームの確保。 AVFrame* pSrcFrame = av_frame_alloc(); int srcWidth = pVideoCodecCtx->width; int srcHeight = pVideoCodecCtx->height; int srcFmt = pVideoCodecCtx->pix_fmt; // コマ画像キャプチャ用フレームの確保。(バッファ付き) AVFrame* pCapFrame = av_frame_alloc(); int capWidth = 320; int capHeight = 180; int capFmt = AV_PIX_FMT_RGB24; int bufferAlign = 32; unsigned char* pCapBuffer = (unsigned char *)av_malloc(av_image_get_buffer_size(capFmt, capWidth, capHeight, bufferAlign)); // バッファ確保 av_image_fill_arrays(pCapFrame->data, pCapFrame->linesize, pCapBuffer, capFmt, capWidth, capHeight, bufferAlign); // バッファ関連付け // Video→コマ画像変換用コンテキストの取得。 struct SwsContext* pSwsCtx = sws_getContext(srcWidth, srcHeight, srcFmt, capWidth, capHeight, capFmt, SWS_FAST_BILINEAR, NULL, NULL, NULL); // フレームを読み込む。 ->pkt AVPacket pkt; int videoCount = 0, audioCount = 0; while (av_read_frame(pFormatCtx, &pkt) == 0) { // Videoフレームのデコード。 pkt -> pSrcFrame if (pkt.stream_index == videoStreamIndex) { int got_picture; if (avcodec_decode_video2(pVideoCodecCtx, pSrcFrame, &got_picture, &pkt) < 0) { printf("Error: avcodec_decode_video2()\n"); } else { // 1フレーム完成したらキャプチャ。 pSrcFrame -> pCapFrame if (got_picture) { ++videoCount; sws_scale(pSwsCtx, (const uint8_t **)pSrcFrame->data, pSrcFrame->linesize, 0 , srcHeight, pCapFrame->data, pCapFrame->linesize); // pCapFrame -> UIImage UIImage* image = [self rgb24ToUIImage:pCapFrame->data[0] width:capWidth height:capHeight wrap:pCapFrame->linesize[0]]; // メインスレッドでコールバック dispatch_async(dispatch_get_main_queue(), ^{ gotFrame(image); }); // デコーダスレッドは少し待つ。だいたい30fpsにする。 [NSThread sleepForTimeInterval:0.029f]; if (videoCount % 1000 == 0) printf("video #%d time:%f\n", videoCount, videoTimeBase * pSrcFrame->pkt_pts); } } } // Audioフレームのデコード。 pkt -> pSrcFrame else if (pkt.stream_index == audioStreamIndex) { do { int got_frame; int ret = avcodec_decode_audio4(pAudioCodecCtx, pSrcFrame, &got_frame, &pkt); if (ret < 0) { printf("Error: avcodec_decode_audio4()\n"); } else { // 1フレーム完成したら表示。 if (got_frame) { if (pAudioCodecCtx->sample_fmt != AV_SAMPLE_FMT_FLTP) { printf("Error: unsupported audio sample format.\n"); continue; } ++audioCount; if (audioCount % 1000 == 0) printf("audio #%d time:%f\n", audioCount, audioTimeBase * pSrcFrame->pkt_pts); } } // コーデックによっては一度に全てを処理しないので残りを再処理。 int decoded = FFMIN(ret, pkt.size); pkt.data += decoded; pkt.size -= decoded; } while (0 < pkt.size) ; } // パケットメモリ解放。 av_packet_unref(&pkt); } printf("Total video frame: %d\n", videoCount); printf("Total audio frame: %d\n", audioCount); // メモリ解放。 sws_freeContext(pSwsCtx); av_free(pCapBuffer); av_free(pCapFrame); av_free(pSrcFrame); avcodec_close(pAudioCodecCtx); avcodec_close(pVideoCodecCtx); avformat_close_input(&pFormatCtx); }); } - (UIImage*)rgb24ToUIImage:(uint8_t*)pRgb width:(int)width height:(int)height wrap:(int)wrap { // RGB24 -> RGBA32 pixelデータ転送。 uint8_t* pRgba = malloc(width * height * 4); for (int y = 0; y < height; ++y) { int si = y * wrap; int di = y * width * 4; for (int x = 0; x < width; ++x) { pRgba[di + 0] = pRgb[si + 0]; pRgba[di + 1] = pRgb[si + 1]; pRgba[di + 2] = pRgb[si + 2]; pRgba[di + 3] = UINT8_MAX; si += 3; di += 4; } } // UIImage作成。 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef ctx = CGBitmapContextCreate(pRgba, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast); CGImageRef cgimage = CGBitmapContextCreateImage(ctx); UIImage* uiimage = [[UIImage alloc] initWithCGImage:cgimage]; CGImageRelease(cgimage); CGContextRelease(ctx); free(pRgba); return uiimage; } @end
a.mp4リソースファイルとして動画を登録しておく。
-
実行結果。
約30fpsで静止画が連続表示され、無音で動画が動くように見える。
-
サーチパスの設定。