iOS Tips #7 画像から動画を作成する | アドカレ2013 : SP #15

2013.12.15

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

かんたんですが、複数枚の画像から動画を作成するロジックを紹介します。以外と紹介されていないので、探されていた方はどうぞ!

この処理に必要なのは、以下の3つのクラスです。

  • AVAssetWriter
  • AVAssetWriterInput
  • AVAssetWriterInputPixelBufferAdaptor

これらのクラスはAVFoundation.frameworkで定義されています。なので、AVFoundation.frameworkをプロジェクトにリンクしておきましょう。

サンプル

それでは早速サンプルを紹介します。画像は以下の5つを使用します。

ios-tips-7-movie-from-images-red

ios-tips-7-movie-from-images-yellow

ios-tips-7-movie-from-images-green

ios-tips-7-movie-from-images-blue

ios-tips-7-movie-from-images-pink


画像をプロジェクトに追加したら、ViewController.mを以下のように変更してください。

#import "ViewController.h"

#import <AVFoundation/AVFoundation.h>

@interface ViewController ()

@property (nonatomic) AVAssetWriter *videoWriter;

@end

@implementation ViewController

// サンプルなのでとりあえずviewDidAppearで実行する
- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    // 動画にする画像
    NSArray *images =
    @[
      [UIImage imageNamed:@"red"],
      [UIImage imageNamed:@"yellow"],
      [UIImage imageNamed:@"green"],
      [UIImage imageNamed:@"blue"],
      [UIImage imageNamed:@"pink"],
      ];
    
    // 動画の出力先
    NSString *documentPath = (NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]);
    NSString *moviePath = [documentPath stringByAppendingPathComponent:@"movie.mov"];

    [self writeImagesAsMovie:images
                      toPath:moviePath];
}

#pragma mark - Movie generator methods

/*!
 画像の配列から動画を生成する。

 @param images 画像の配列
 @param path 動画ファイルの出力先
 */
- (void)writeImagesAsMovie:(NSArray *)images
                    toPath:(NSString *)path
{
    NSParameterAssert(images);
    NSParameterAssert(path);
    NSAssert((images.count > 0), @"Set least one image.");
    
    NSFileManager *fileManager = [NSFileManager defaultManager];
        
    // 既にファイルがある場合は削除する
    if ([fileManager fileExistsAtPath:path]) {
        [fileManager removeItemAtPath:path error:nil];
    }
    
    // 最初の画像から動画のサイズ指定する
    CGSize size = ((UIImage *)images[0]).size;
    
    NSError *error = nil;
    
    self.videoWriter = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:path]
                                                 fileType:AVFileTypeQuickTimeMovie
                                                    error:&error];
    
    if (error) {
        NSLog(@"%@", [error localizedDescription]);
        return;
    }
    
    NSDictionary *outputSettings =
    @{
      AVVideoCodecKey  : AVVideoCodecH264,
      AVVideoWidthKey  : @(size.width),
      AVVideoHeightKey : @(size.height),
      };
    
    AVAssetWriterInput *writerInput = [AVAssetWriterInput
                                       assetWriterInputWithMediaType:AVMediaTypeVideo
                                       outputSettings:outputSettings];
    
    [self.videoWriter addInput:writerInput];
    
    NSDictionary *sourcePixelBufferAttributes =
    @{
      (NSString *)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32ARGB),
      (NSString *)kCVPixelBufferWidthKey           : @(size.width),
      (NSString *)kCVPixelBufferHeightKey          : @(size.height),
      };
    
    AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor
                                                     assetWriterInputPixelBufferAdaptorWithAssetWriterInput:writerInput
                                                     sourcePixelBufferAttributes:sourcePixelBufferAttributes];
    
    writerInput.expectsMediaDataInRealTime = YES;
    
    // 動画生成開始
    if (![self.videoWriter startWriting]) {
        NSLog(@"Failed to start writing.");
        return;
    }
    
    [self.videoWriter startSessionAtSourceTime:kCMTimeZero];
    
    CVPixelBufferRef buffer = NULL;
    
    int frameCount = 0;
    int durationForEachImage = 1;
    int32_t fps = 24;
    
    for (UIImage *image in images) {
        if (adaptor.assetWriterInput.readyForMoreMediaData) {
            CMTime frameTime = CMTimeMake((int64_t)frameCount * fps * durationForEachImage, fps);
            
            buffer = [self pixelBufferFromCGImage:image.CGImage];
            
            if (![adaptor appendPixelBuffer:buffer withPresentationTime:frameTime]) {
                NSLog(@"Failed to append buffer. [image : %@]", image);
            }
            
            if(buffer) {
                CVBufferRelease(buffer);
            }
            
            frameCount++;
        }
    }
    
    // 動画生成終了
    [writerInput markAsFinished];
    [self.videoWriter finishWritingWithCompletionHandler:^{
        NSLog(@"Finish writing!");
    }];
    CVPixelBufferPoolRelease(adaptor.pixelBufferPool);
}

- (CVPixelBufferRef)pixelBufferFromCGImage:(CGImageRef)image
{
    NSDictionary *options = @{
                              (NSString *)kCVPixelBufferCGImageCompatibilityKey : @(YES),
                              (NSString *)kCVPixelBufferCGBitmapContextCompatibilityKey: @(YES),
                              };
    
    CVPixelBufferRef pxbuffer = NULL;
    
    CVPixelBufferCreate(kCFAllocatorDefault,
                        CGImageGetWidth(image),
                        CGImageGetHeight(image),
                        kCVPixelFormatType_32ARGB,
                        (__bridge CFDictionaryRef)options,
                        &pxbuffer);
    
    CVPixelBufferLockBaseAddress(pxbuffer, 0);
    void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
    
    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(pxdata,
                                                 CGImageGetWidth(image),
                                                 CGImageGetHeight(image),
                                                 8,
                                                 4 * CGImageGetWidth(image),
                                                 rgbColorSpace,
                                                 (CGBitmapInfo)kCGImageAlphaNoneSkipFirst);
    
    CGContextConcatCTM(context, CGAffineTransformMakeRotation(0));
    
    CGAffineTransform flipVertical = CGAffineTransformMake(1, 0, 0, -1, 0, CGImageGetHeight(image));
    
    CGContextConcatCTM(context, flipVertical);
    
    CGAffineTransform flipHorizontal = CGAffineTransformMake(-1.0, 0.0, 0.0, 1.0, CGImageGetWidth(image), 0.0);
    
    CGContextConcatCTM(context, flipHorizontal);
    
    CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image), CGImageGetHeight(image)), image);
    CGColorSpaceRelease(rgbColorSpace);
    CGContextRelease(context);
    
    CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
    
    return pxbuffer;
}

@end

これを実行すると、iOS端末のドキュメントディレクトリ(シミュレータの場合は/Users/ユーザ名/Library/Application Support/iPhone Simulator/7.x.x/アプリID/Documents/)にmovie.movが生成されます。これをQuickTimeなどで再生すると以下のように赤>黃>緑>青>ピンクの順にそれぞれ1秒ずつ表示されます。

みなさんお好きなようにいじってみてください!ではまた。