[Objective-C] AVFoundationを使って複数の動画を結合する


動画を結合する必要があったので色々調べてみました。
しかし、使うクラスが結構あってしかもそれぞれがどういう役割かをしっかり把握しないとならいないから大変です( ;´Д`)

最低限のコードでふたつの動画を単純に結合するだけのサンプルを作りました。
今回作った[動画合成のサンプルをGitHub](https://github.com/edom18/AVCompositionSample)に上げてあるのでよかったら見てみてください。

## 使用するクラス郡

- AVURLAsset
- AVAssetTrack
- AVMutableComposition
- AVMutableCompositionTrack
- AVMutableVideoCompositionInstruction
- AVMutableVideoCompositionLayerInstruction
- AVMutableVideoComposition
- AVAssetExportSession
- AVMutableAudioMix
- AVMutableAudioMixInputParameters



### 各種クラスの相関図

[ドキュメント](https://developer.apple.com/library/ios/documentation/AudioVideo/Conceptual/AVFoundationPG/Articles/03_Editing.html)から引用させてもらうと、以下の様な相関関係があります。

![avmutablecomposition_2x.png](https://qiita-image-store.s3.amazonaws.com/0/5616/36d76881-e11b-0059-0849-5820af4b086e.png "avmutablecomposition_2x.png")

![avmutableaudiomix_2x.png](https://qiita-image-store.s3.amazonaws.com/0/5616/1f26e4b8-655a-c5df-76c4-3fe81df83543.png "avmutableaudiomix_2x.png")

![avmutablevideocomposition_2x.png](https://qiita-image-store.s3.amazonaws.com/0/5616/7810650e-4812-ad44-6d68-7c07dfaabeb1.png "avmutablevideocomposition_2x.png")

![puttingitalltogether_2x.png](https://qiita-image-store.s3.amazonaws.com/0/5616/2fe42a0e-8280-8598-ca0c-2d15b4385b77.png "puttingitalltogether_2x.png")


### 大まかなカテゴリ分け

あくまで自分の理解で、という意味ですが以下のように考えると役割が把握しやすいかなと。

#### 合成処理の本体

`AVMutableComposition`

このクラスが基本的に合成処理を担当します。


#### トラック情報

`AVMutableCompositionTrack`

ちなみにトラックの意味を調べたらこんな意味でした↓

>トラック 【track】
磁気テープやディスクなどの記録メディアに映像や音声の信号を記録する帯状の領域のこと。実際にはデータが断続しているディジタル信号の記録でも、イメージとしては各信号ごとに帯状の領域となっている。

[引用](http://www.weblio.jp/content/%E3%83%88%E3%83%A9%E3%83%83%E3%82%AF)

##### 映像・音声の合成処理

`AVMutableVideoComposition`
`AVMutableAudioMix`


#### 情報の設定、表現

動画には映像部分と音声部分があり、さらにそれらには時間や画角、ボリュームなど様々な情報が必要です。
それらを表すのが以下のクラス。

[映像]

映像の合成に使われる設定（命令）です。
※ `Instruction`は「命令」という意味。

`AVMutableVideoCompositionInstruction`
`AVMutableVideoCompositionLayerInstruction`


[音声]

こちらは音声の情報です。ボリュームとかですね。

`AVMutableAudioMixInputParameters`


#### 動画の出力（Export）

こちらはそのままの意味なので分かりやすいですね。

`AVAssetExportSession`

-------------------------------------------

## 2つの動画を合成する大まかな流れ

1. 合成を実行する`AVMutableComposition`オブジェクトを作る
2. `AVAsset`オブジェクトの生成と、オブジェクトから動画部分と音声部分のトラック情報をそれぞれ取得する
3. トラック合成用の`AVMutableCompositionTrack`を、`AVMutableComposition`から生成する
4. (3)で生成したトラックに動画・音声を登録する
5. 動画の合成命令用オブジェクトを生成（`AVMutableVideoCompositionInstruction`と`AVMutableVideoCompositionLayerInstruction`）
6. 動画合成オブジェクト（`AVMutableVideoComposition`）を生成
7. 音声合成用パラメータオブジェクトの生成（`AVMutableAudioMixInputParameters`）
8. 音声合成用オブジェクトを生成（`AVMutableAudioMix`）
9. 動画情報（`AVAssetTrack`）から回転状態を判別する
10. 回転情報を元に、合成、生成する動画のサイズを決定する
11. 合成動画の情報を設定する
12. 動画出力用オブジェクトを生成する
13. 保存設定を行い、Exportを実行

という流れになります。



## サンプルコード

ちょっと長いですが、今回書いたコードを載せておきます。
[実際に動作するサンプルはGitHub](https://github.com/edom18/AVCompositionSample)に上がっています。

インスタンスを生成して`create:`メソッドを実行すると、プロジェクト内にある動画ふたつを合成して保存するだけのものです。

それぞれなにをしているかはコード内のコメントに記載しています。

```objc

@import UIKit;
@import AVFoundation;
@import MediaPlayer;

#import "Composition.h"

@interface Composition ()
@property (nonatomic, copy) void (^handler)(NSURL *url);
@end


@implementation Composition

- (void)create:(void (^)(NSURL *url))handler
{
    self.handler = handler;
    
    /////////////////////////////////////////////////////////////////////////////
    // 手順1
    
    // Compositionを生成
    AVMutableComposition *mutableComposition = [AVMutableComposition composition];
    
    /////////////////////////////////////////////////////////////////////////////
    // 手順2
    
    // AVAssetをURLから取得
    AVURLAsset *videoAsset1 = [[AVURLAsset alloc] initWithURL:self.movieFile1 options:nil];
    AVURLAsset *videoAsset2 = [[AVURLAsset alloc] initWithURL:self.movieFile2 options:nil];
    
    // アセットから動画・音声トラックをそれぞれ取得
    AVAssetTrack *videoAssetTrack1 = [videoAsset1 tracksWithMediaType:AVMediaTypeVideo][0];
    AVAssetTrack *audioAssetTrack1 = [videoAsset1 tracksWithMediaType:AVMediaTypeAudio][0];
    
    AVAssetTrack *videoAssetTrack2 = [videoAsset2 tracksWithMediaType:AVMediaTypeVideo][0];
    AVAssetTrack *audioAssetTrack2 = [videoAsset2 tracksWithMediaType:AVMediaTypeAudio][0];
    
    /////////////////////////////////////////////////////////////////////////////
    // 手順3
    
    // 動画合成用の`AVMutableCompositionTrack`を生成
    AVMutableCompositionTrack *compositionVideoTrack = [mutableComposition addMutableTrackWithMediaType:AVMediaTypeVideo
                                                                                       preferredTrackID:kCMPersistentTrackID_Invalid];
    // 音声合成用の`AVMutableCompositionTrack`を生成
    AVMutableCompositionTrack *compositionAudioTrack = [mutableComposition addMutableTrackWithMediaType:AVMediaTypeAudio
                                                                                       preferredTrackID:kCMPersistentTrackID_Invalid];
    
    /////////////////////////////////////////////////////////////////////////////
    // 手順4
    
    // ひとつめの動画をトラックに追加
    // `videoAssetTrack1`の動画の長さ分を`kCMTimeZero`の位置に挿入
    [compositionVideoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAssetTrack1.timeRange.duration)
                                   ofTrack:videoAssetTrack1
                                    atTime:kCMTimeZero
                                     error:nil];
    // ひとつめの音声をトラックに追加
    [compositionAudioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, audioAssetTrack1.timeRange.duration)
                                   ofTrack:audioAssetTrack1
                                    atTime:kCMTimeZero
                                     error:nil];
    
    // ふたつめの動画を追加
    // `videoAssetTrack2`の動画の長さ分を`videoAssetTrack1`の終了時間の後ろに挿入
    [compositionVideoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAssetTrack2.timeRange.duration)
                                   ofTrack:videoAssetTrack2
                                    atTime:videoAssetTrack1.timeRange.duration
                                     error:nil];
    // ふたつめの音声を追加
    [compositionAudioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, audioAssetTrack2.timeRange.duration)
                                   ofTrack:audioAssetTrack2
                                    atTime:audioAssetTrack1.timeRange.duration
                                     error:nil];
    
    /////////////////////////////////////////////////////////////////////////////
    // 手順5
    
    // Video1の合成命令用オブジェクトを生成
    AVMutableVideoCompositionInstruction *mutableVideoCompositionInstruction1 = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
    mutableVideoCompositionInstruction1.timeRange = CMTimeRangeMake(kCMTimeZero,
                                                                    videoAssetTrack1.timeRange.duration);
    mutableVideoCompositionInstruction1.backgroundColor = UIColor.redColor.CGColor;
    
    // Video1のレイヤーの合成命令を生成
    AVMutableVideoCompositionLayerInstruction *videoLayerInstruction1;
    videoLayerInstruction1= [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:compositionVideoTrack];
    mutableVideoCompositionInstruction1.layerInstructions = @[videoLayerInstruction1];
    
    // Video2の合成命令用オブジェクトを生成
    AVMutableVideoCompositionInstruction *mutableVideoCompositionInstruction2 = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
    mutableVideoCompositionInstruction2.timeRange = CMTimeRangeMake(videoAssetTrack1.timeRange.duration,
                                                                    CMTimeAdd(videoAssetTrack1.timeRange.duration, videoAssetTrack2.timeRange.duration));
    mutableVideoCompositionInstruction2.backgroundColor = UIColor.blueColor.CGColor;
    
    // Video2のレイヤーの合成命令を生成
    AVMutableVideoCompositionLayerInstruction *videoLayerInstruction2;
    videoLayerInstruction2= [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:compositionVideoTrack];
    mutableVideoCompositionInstruction2.layerInstructions = @[videoLayerInstruction2];
    
    /////////////////////////////////////////////////////////////////////////////
    // 手順6
    
    // AVMutableVideoCompositionを生成
    AVMutableVideoComposition *mutableVideoComposition = [AVMutableVideoComposition videoComposition];
    mutableVideoComposition.instructions = @[mutableVideoCompositionInstruction1, mutableVideoCompositionInstruction2];
    
    /////////////////////////////////////////////////////////////////////////////
    // 手順7
    
    // Audioの合成パラメータオブジェクトを生成
    AVMutableAudioMixInputParameters *audioMixInputParameters;
    audioMixInputParameters = [AVMutableAudioMixInputParameters audioMixInputParametersWithTrack:compositionAudioTrack];
    [audioMixInputParameters setVolumeRampFromStartVolume:1.0
                                              toEndVolume:1.0
                                                timeRange:CMTimeRangeMake(kCMTimeZero, mutableComposition.duration)];
    
    /////////////////////////////////////////////////////////////////////////////
    // 手順8
    
    // AVMutableAudioMixを生成
    AVMutableAudioMix *mutableAudioMix = [AVMutableAudioMix audioMix];
    mutableAudioMix.inputParameters = @[audioMixInputParameters];
    
    /////////////////////////////////////////////////////////////////////////////
    // 手順9
    
    // 動画の回転情報を取得する
    CGAffineTransform transform1 = videoAssetTrack1.preferredTransform;
    BOOL isVideoAssetPortrait = ( transform1.a == 0 &&
                                  transform1.d == 0 &&
                                 (transform1.b == 1.0 || transform1.b == -1.0) &&
                                 (transform1.c == 1.0 || transform1.c == -1.0));
    
    /////////////////////////////////////////////////////////////////////////////
    // 手順10
    
    CGSize naturalSize1 = CGSizeZero;
    CGSize naturalSize2 = CGSizeZero;
    if (isVideoAssetPortrait) {
        naturalSize1 = CGSizeMake(videoAssetTrack1.naturalSize.height, videoAssetTrack1.naturalSize.width);
        naturalSize2 = CGSizeMake(videoAssetTrack2.naturalSize.height, videoAssetTrack2.naturalSize.width);
    }
    else {
        naturalSize1 = videoAssetTrack1.naturalSize;
        naturalSize2 = videoAssetTrack2.naturalSize;
    }
    
    CGFloat renderWidth  = MAX(naturalSize1.width, naturalSize2.width);
    CGFloat renderHeight = MAX(naturalSize1.height, naturalSize2.height);
    
    /////////////////////////////////////////////////////////////////////////////
    // 手順11
    
    // 書き出す動画のサイズ設定
    mutableVideoComposition.renderSize = CGSizeMake(renderWidth, renderHeight);
    
    // 書き出す動画のフレームレート（30FPS）
    mutableVideoComposition.frameDuration = CMTimeMake(1, 30);
    
    /////////////////////////////////////////////////////////////////////////////
    // 手順12
    
    // AVMutableCompositionを元にExporterの生成
    AVAssetExportSession *assetExportSession = [[AVAssetExportSession alloc] initWithAsset:mutableComposition
                                                                                presetName:AVAssetExportPreset1280x720];
    // 動画合成用のオブジェクトを指定
    assetExportSession.videoComposition = mutableVideoComposition;
    assetExportSession.audioMix         = mutableAudioMix;
    
    // エクスポートファイルの設定
    NSString *composedMovieDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
    NSString *composedMoviePath      = [NSString stringWithFormat:@"%@/%@", composedMovieDirectory, @"test.mp4"];
    
    // すでに合成動画が存在していたら消す
    NSFileManager *fileManager = NSFileManager.defaultManager;
    if ([fileManager fileExistsAtPath:composedMoviePath]) {
        [fileManager removeItemAtPath:composedMoviePath error:nil];
    }
    
    // 保存設定
    NSURL *composedMovieUrl = [NSURL fileURLWithPath:composedMoviePath];
    assetExportSession.outputFileType              = AVFileTypeQuickTimeMovie;
    assetExportSession.outputURL                   = composedMovieUrl;
    assetExportSession.shouldOptimizeForNetworkUse = YES;
    
    // 動画をExport
    [assetExportSession exportAsynchronouslyWithCompletionHandler:^{
        switch (assetExportSession.status) {
            case AVAssetExportSessionStatusFailed: {
                NSLog(@"生成失敗");
                break;
            }
            case AVAssetExportSessionStatusCancelled: {
                NSLog(@"生成キャンセル");
                break;
            }
            default: {
                NSLog(@"生成完了");
                if (self.handler) {
                    self.handler(composedMovieUrl);
                }
                break;
            }
        }
    }];
}


/**
 *  合成するひとつめの動画ファイル
 */
- (NSURL *)movieFile1
{
    NSBundle *bundle = NSBundle.mainBundle;
    NSString *path   = [bundle pathForResource:@"URLHookmark" ofType:@"mp4"];
    NSURL *url       = [NSURL fileURLWithPath:path];
    return url;
}


/**
 *  合成するふたつめの動画ファイル
 */
- (NSURL *)movieFile2
{
    NSBundle *bundle = NSBundle.mainBundle;
    NSString *path   = [bundle pathForResource:@"FurShader" ofType:@"mp4"];
    NSURL *url       = [NSURL fileURLWithPath:path];
    return url;
}

@end

```

本来は動画の合成だけじゃなくて、音声を合成してアフレコにしたり、静止画を上に重ねて表示、とかきっとそういうことをやるんだと思います。
が、まずは最低限動作するコードを把握しておかないと色々混乱するのでまとめてみました。

-------------------------------------------

## 参考URL

- [AV Foundation Programming Guide](https://developer.apple.com/library/ios/documentation/AudioVideo/Conceptual/AVFoundationPG/Articles/03_Editing.html)
- [AV Foundation Frameworkを利用して動画の結合を行う](http://nakajijapan.github.io/blog/2013/10/22/how-to-combine-some-movies-with-avfoundation/)