大分間が空いてしまったが、AVFoundationを使った動画編集について、今回は映像Trackからフレームの静止画を取り出す部分まで試す。
AVReaderWriter for OSXを読む
Appleが公式で用意しているサンプル(AVReaderWriter for OSX)に、AV Foundationを使った動画編集に関して、大凡知りたいポイントの基本が詰まっているので、このコードを読んでみるのが良い。
AVReaderWriter for OSXは、
- ソースのムービーを読み込み
- 映像トラックに加工を施し
- 別の新しいムービーファイルとして書き出す
という処理を行っている。
ただし、このサンプルプロジェクトは、昨今のiOSアプリ開発に慣れている人から見ると、コーディングスタイルの古さや、OSXアプリ特有のUI関連/非同期処理関連の都合で若干読みにくいと思う。
そこで、重要な部分だけを読んでいく。まずは、表題のサムネイル取得部分まで。
Assetの読み込み
AVReaderWriterはDocument-based Applicationとして実装されている。
Openメニューから開いた各ファイルは、それぞれRWDocumentクラスのインスタンス上で扱われるので、今回見ていくのもRWDocumentクラスが主となる。
RWDocumentクラスの中で、最初に注目するべきオブジェクトは、asset
。
@interface RWDocument : NSDocument
{
@private
...
AVAsset *asset;
...
}
asset
の実体は、readFromURL:ofType:error:
内で生成されている。
readFromURL:ofType:error:
は、Cocoa Document-based Applicationで、ファイルの読み込みをした際に呼ばれるメソッドだが、今回のテーマの本質ではないのでどこから呼ばれているのか、などは気にしなくてよい。
NSDictionary *assetOptions = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
AVAsset *localAsset = [AVURLAsset URLAssetWithURL:url options:assetOptions];
[self setAsset:localAsset];
Assetの読み込みに関しては、前回の投稿に書いた内容と同じ。特別な事はしていない。
AVAssetImageGenerator
次に注目するオブジェクトはAVAssetImageGenerator
クラスのインスタンスimageGenerator
。
imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:localAsset];
で、読み込んだAsset
を引数にインスタンスを生成し、
// Grab the first frame from the asset and display it
[imageGenerator generateCGImagesAsynchronouslyForTimes:[NSArray arrayWithObject:[NSValue valueWithCMTime:kCMTimeZero]] completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError *error) {
if (result == AVAssetImageGeneratorSucceeded)
[self setPreviewLayerContents:(id)image gravity:kCAGravityResizeAspect];
else
[self setPreviewLayerContents:[NSImage imageNamed:@"ErrorLoading2x"] gravity:kCAGravityCenter];
}];
windowControllerDidLoadNib:
メソッド(RWDocument
に対応するウィンドウが生成されたタイミングで呼ばれる)内で、プレビュー用画像生成に使われている。
AVAssetImageGeneratorとは
AVAssetImageGenerator Class Referenceによると、
An AVAssetImageGenerator object provides thumbnail or preview images of assets independently of playback.
とのことなので、Assetのサムネイルやプレビュー画像を提供してくれるが、基本的にはプレビュー用途で使われるもので、実際にAVReaderWriter for OSXの中でも、AVAssetImageGeneratorは最初のプレビュー画像の表示の処理以外では使われていない。
サムネイルの取得
AVReaderWriter for OSXでは、generateCGImagesAsynchronouslyForTimes:completionHandler:
を使ってサムネイル画像を取得している。
generateCGImagesAsynchronouslyForTimes:completionHandler:
では、一度に複数枚のサムネイル取得をリクエストすることができるため、若干読みにくくなっているが、ここでは1枚(指定時刻:kCMTimeZero = ムービーの初頭フレーム)のサムネイル画像を取得している。
ここで取得される画像は、CGImageRef
型で、setPreviewLayerContents:gravity:
メソッド内でCALayerのコンテンツに設定している。
- (void)setPreviewLayerContents:(id)contents gravity:(NSString *)gravity
{
CALayer *localFrameLayer = [[self frameView] layer];
[CATransaction begin]; // need a transaction since we are not executing on the main thread
{
[localFrameLayer setContents:contents];
[localFrameLayer setContentsGravity:gravity];
}
[CATransaction commit];
}
CATransaction
を使う形になっているのでここも若干本質とは違う部分で行数が増えているが、本質的な部分はソースのAssetを指定したAVAssetImageGeneratorを使うと、指定した時刻のサムネイル画像を取得できるという、極めてシンプルな話。
実はAVAssetImageGenerator
には、同じ用途でcopyCGImageAtTime:actualTime:error:
という同期型のシンプルなメソッドも用意されていて、こちらを使うとよりわかりやすく書ける。(ただし、スレッドの処理をブロックしてしまうことになるので、実際のプロダクトレベルのアプリではサンプル同様非同期型のgenerateCGImagesAsynchronouslyForTimes:completionHandler:
を使うべき)
CGImageRef thumbImage = [imageGenerator copyCGImageAtTime:kCMTimeZero actualTime:NULL error:&error];
サムネイル取得のタイミング指定
AVAssetTrack *videoTrack = visualTracks[0];
CMTime requestTime = CMTimeMakeWithSeconds(CMTimeGetSeconds(videoTrack.timeRange.duration), 600)
NSError *error = nil;
CGImageRef thumbImage = [imageGenerator copyCGImageAtTime:requestTime actualTime:NULL error:&error];
サムネイルを取得したいタイミング(時間)は、CMTime
型で指定できる。
取得できるサムネイルのタイミングについて
ただし、AVAssetImageGenerator
を使って取得できるサムネイルのタイミングは、必ずしも指定した時刻ピッタリのものが取得できるわけではないようだ。実際、generateCGImagesAsynchronouslyForTimes:completionHandler:
、copyCGImageAtTime:actualTime:error:
どちらを利用した場合でも、取得時にはactualTime
という変数の中に取得したサムネイルの実際の時刻が含まれるようになっている。
こういったことからも、AVAssetImageGenerator
はあくまでプレビュー用のサムネイル取得目的でしかつかえないと考える必要がある。