お使いのブラウザは、バージョンが古すぎます。

このサイトは、Internet Explore8・Internet Explore9には対応しておりません。
恐れ入りますが、お使いのブラウザをバージョンアップしていただきますよう宜しくお願いいたします。

iPhoneで動作する映像フィルタをつくる:GPUImage+iOSのAVFoundationフレームワークその3

こんにちは、andyです。

前回の記事で、次回は音声の扱いと予告していたのですが、実はGPUImageMovieで音声を取り扱う場合にバグがあることがネットなどで調べてわかりましたので一旦やめます。
音声に関しては、AVFoundationで扱っておいてフィルタをレンダリングした映像と最後にあわせて出力した方が良いみたいです。


ということで、今回は前回のプロジェクトを引き続き使用してムービーの書き出しを行ってみます。

ムービー書き出しのコード

それでは今回のコードです。

#import "VideoFilterViewController.h"

@interface VideoFilterViewController ()
{
    GPUImageMovieComposition *movieFile;
    
    //解説-1
    GPUImageView *filterView;
    AVMutableComposition* composition;
    AVMutableVideoComposition* transformVideoComposition;
    GPUImagePolkaDotFilter *filter;
    GPUImageMovieWriter* movieWriter;
}

@end

@implementation VideoFilterViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
	[self showFilteringMovie];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

- (void)showFilteringMovie
{
    NSURL *fileURL1 = [[NSBundle mainBundle] URLForResource:@"<一番目の動画>" withExtension:@"mp4"];
    NSURL *fileURL2 = [[NSBundle mainBundle] URLForResource:@"<二番目の動画>" withExtension:@"mp4"];
    
    composition = [AVMutableComposition composition];
    AVMutableCompositionTrack* videoTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
    
    AVURLAsset* firstMovie = [AVURLAsset URLAssetWithURL:fileURL1 options:nil];
    AVURLAsset* secondMovie = [AVURLAsset URLAssetWithURL:fileURL2 options:nil];
    
    AVAssetTrack* firstMovieVideo = [[firstMovie tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
    AVAssetTrack* secondMovieVideo = [[secondMovie tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
    
    [videoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, firstMovie.duration)
                        ofTrack:firstMovieVideo
                         atTime:kCMTimeZero
                          error:nil];
    [videoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, secondMovie.duration)
                        ofTrack:secondMovieVideo
                         atTime:firstMovie.duration
                          error:nil];

    transformVideoComposition = [AVMutableVideoComposition videoComposition];
    NSMutableArray *inst = [NSMutableArray array];
    
    CGSize frameSize = CGSizeMake(640.0f, 480.0f);
    float scale = 0.7f;

    AVMutableVideoCompositionInstruction * firstInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
    firstInstruction.backgroundColor = [[UIColor blueColor] CGColor];
    firstInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, firstMovie.duration);

    AVMutableVideoCompositionLayerInstruction *firstlayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:firstMovieVideo];
    CGAffineTransform firstLayerScale = CGAffineTransformMakeScale(scale, scale);
    CGAffineTransform firstLayerMove = CGAffineTransformMakeTranslation((frameSize.width - firstMovieVideo.naturalSize.width * scale) / 2,
                                                              (frameSize.height-firstMovieVideo.naturalSize.height * scale) / 2);
    [firstlayerInstruction setTransform:CGAffineTransformConcat(firstLayerScale, firstLayerMove) atTime:kCMTimeZero];

    firstInstruction.layerInstructions = [NSArray arrayWithObjects:firstlayerInstruction,nil];;
    [inst addObject:firstInstruction];
    
    AVMutableVideoCompositionInstruction * secondInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
    secondInstruction.backgroundColor = [[UIColor blueColor] CGColor];
    secondInstruction.timeRange = CMTimeRangeMake(firstMovie.duration, secondMovie.duration);
    
    AVMutableVideoCompositionLayerInstruction *secondlayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:secondMovieVideo];
    CGAffineTransform secondLayerScale = CGAffineTransformMakeScale(scale, scale);
    CGAffineTransform secondLayerMove = CGAffineTransformMakeTranslation((frameSize.width - secondMovieVideo.naturalSize.width * scale) / 2,
                                                              (frameSize.height - secondMovieVideo.naturalSize.height * scale) / 2);
    [secondlayerInstruction setTransform:CGAffineTransformConcat(secondLayerScale, secondLayerMove) atTime:kCMTimeZero];
    
    secondInstruction.layerInstructions = [NSArray arrayWithObjects:secondlayerInstruction,nil];;
    [inst addObject:secondInstruction];
    
    transformVideoComposition.instructions = inst;

    transformVideoComposition.renderSize = frameSize;
    transformVideoComposition.frameDuration = CMTimeMake(1, 30);
    
    filterView = [[GPUImageView alloc] initWithFrame:CGRectMake(0, 0, 320, 240)];
    filter = [[GPUImagePolkaDotFilter alloc] init];

    //movieFile = [[GPUImageMovie alloc] initWithAsset:composition];

    movieFile = [[GPUImageMovieComposition alloc] initWithComposition:composition andVideoComposition:transformVideoComposition andAudioMix:nil];
    movieFile.playAtActualSpeed = YES;

    [movieFile addTarget:filter];
    [filter addTarget:filterView];
    [self.view addSubview:filterView];
    
    [NSTimer scheduledTimerWithTimeInterval:0.5f
                                     target:self
                                   selector: @selector(play:)
                                   userInfo:nil
                                    repeats:NO];
    
}

- (void)play:(id)sender
{
    //解説-2
    __weak GPUImageMovie* weakMovieFile = movieFile;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [weakMovieFile startProcessing];
    });
}

//解説-3
- (IBAction)exportToMovie:(id)sender;
{
    if (filterView == nil) {
        filterView = [[GPUImageView alloc] initWithFrame:CGRectMake(0, 0, 320, 240)];
    }
    
    if (movieFile) {
        [movieFile cancelProcessing];
        [movieFile removeAllTargets];
        [filter removeAllTargets];
        movieFile = nil;
        [filterView removeFromSuperview];
    }
    
    //解説-4
    movieFile = [[GPUImageMovieComposition alloc] initWithComposition:composition andVideoComposition:transformVideoComposition andAudioMix:nil];
    movieFile.playAtActualSpeed = NO;

    [movieFile addTarget:filter];
    [filter addTarget:filterView];
    [self.view addSubview:filterView];
    
    //解説-5
    NSString* pathToMovie = [NSHomeDirectory() stringByAppendingPathComponent:@"tmp/tmp.mov"];
    unlink([pathToMovie UTF8String]);
    NSURL *movieURL = [NSURL fileURLWithPath:pathToMovie];
    
    //解説−6
    CGSize frame = CGSizeMake(640, 480);
    
    //解説-7
    movieWriter = [[GPUImageMovieWriter alloc] initWithMovieURL:movieURL size:frame fileType:AVFileTypeQuickTimeMovie outputSettings:nil];
    
    [filter addTarget:movieWriter];
    [movieFile enableSynchronizedEncodingUsingMovieWriter:movieWriter];
    
    [NSTimer scheduledTimerWithTimeInterval:1.0f
                                     target:self
                                   selector: @selector(exportStart:)
                                   userInfo:nil
                                    repeats:NO];
}

//解説-8
- (void)exportStart:(id)sender
{
    //解説-9
    __weak GPUImageMovieWriter* weakMovieWriter = movieWriter;
    __weak GPUImageMovie* weakMovieFile = movieFile;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [weakMovieWriter startRecording];
        [weakMovieFile startProcessing];
    });
}

@end



作業に入る前に、storyboardで画面上に書き出し用のボタンを追加しておいてください。このボタンがある前提で説明を進めます。

コード解説

解説-1

いくつかの変数のスコープを変更しています。ムービー書き出し用のメソッドでもこれらの変数を参照します。

解説−2

前回のムービー再生に使用したメソッドをスレッド化しています。これを行わないと、ムービー再生中に書き出しボタンが反応できなくなります。

解説-3

このメソッドを先ほど追加したボタンから呼び出せる様に設定してください。

解説-4

GPUImageMovieCompositionを新たに作成しています。再生しているオブジェクトをそのまま使用できるかと思ってやってみたんですが、うまくいかなかったので、ムービー書き出しボタンが押されたときに新たに作り直しています。

解説-5

ファイルの書き出し先を設定しています。今回は、アプリケーションサンドボックス内にあるtmpフォルダにtmp.movとして保存しています。

解説-6

書き出されるムービーの解像度を指定しています。今回は640 x 480にしています。

解説-7

ファイル書き出しを行うためのクラスGPUImageMovieWriterをインスタンス化しています。このmovieWriterにフィルタのインスタンス(filter)を追加することでムービー再生中に書き出しが行われます。

解説-8

ムービー書き出しをスタートするためのメソッドです。先にmovieWriterのプロセスを開始しておきムービーの再生を行います。


こんな感じのコードになります。説明のためにループを使わなかったのですが、かなりダメダメなコードになってしまいました。ご勘弁を。
本当ならば、音声も一緒に扱って圧縮を1回ですませた方が品質が良いのですが、数日間悩んだ末にネット上でブログにバグとの記述を見つけましたので諦めました。


次回は、複数のフィルタを一緒に使う方法を書きたいと考えていますが、実用的かどうかやってみてからにします。


いろいろと突っ込みどころ満載ですが、ご意見など頂けるとうれしいです。
それでは。

コメントをどうぞ

メールアドレスは公開されません。* が付いている欄は必須項目です。


お気軽にお問い合わせください。

日本VTR実験室では、お仕事のご依頼、ブログ・コラムのご感想などを受け付けております。
アプリ開発・コンテンツ制作でお困りでしたら、お気軽にご相談ください。
ご連絡お待ちしております。

お問い合わせはこちらから

03-3541-1230

info@nvtrlab.jp

電話受付対応時間:平日AM9:30〜PM6:00