［Unity］色々な方法で動画作成

Unityでプレイ画面を動画として保存する機能が欲しかったので調べました。
検証したが、動作しないものもありましたのでどこがいけなかったかご指摘ください。

- 基本的にスクリーンキャプチャの連番画像を作成し、動画へ変換の流れです。
- 録画中のプレイ音声やマイク音声は含んでおりません。
- 各方法に画質や速度などを書いています。
    - 画質,速度に関しては元画像の画質やサイズに対しての比較です。
    - 検証環境に入っているが項目が「...」のものは動作するかもしれないが実際に確認していないものです。

#ffmpeg（Windows動作確認済み）
<img width="30" alt="ffmpeg_logo" src="https://qiita-image-store.s3.amazonaws.com/0/82938/6d82eb93-55b8-6b49-2a25-e95cd9e05a92.png">　[ffmpeg](https://www.ffmpeg.org)というアプリを利用しての動画変換です。
外部アプリとして非表示で動作させ引数を渡し変換してもらう作り。
Windowsでは動作確認しましたがMac版もダウンロードはできましたが未検証です。
iOSやAndroidなどに関しては.exeや.appを実行することができないので利用不可だと思います。
また、.mp4から.movや.gifへの変換なども可能ですがここでは割愛します。

###検証結果
| 検証環境 | 動作 | 画質 | 速度 |
|:--|:-:|:-:|:-:|
| Windows(.exe) | 成功 | 悪い | 遅い |
| Mac(.app) | ... | ... | ... |

###検証内容
```csharp:ffmpeg
public void movieConvert()
{
    int framerate = (int)(【キャプチャ枚数】 / 5);
    var arguments = string.Format("-r {2} -i {0}screenshot_%d.png -vcodec libx264 -pix_fmt yuv420p \"{1}\"", 【キャプチャ画像のフォルダパス】, 【動画の書き出し先フォルダパス】 + "movie.mp4", framerate);
    exProcess = new Process();
    exProcess.StartInfo.CreateNoWindow = true;
    exProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
    exProcess.StartInfo.FileName = 【ffmpegのパス】;
    exProcess.StartInfo.Arguments = arguments;
    exProcess.EnableRaisingEvents = true;

    //外部のプロセスを実行する
    exProcess.Start();
    exProcess.WaitForExit();

    【変換後の処理】
}
```
- 説明
    - 【】の中を書き換えて使用してください。
    - 【キャプチャ画像のフォルダパス】にはscreenshot_0.pngから始まる連番画像が入っています。
        - Read and Writeなフォルダを指定してください。
    - 【キャプチャ枚数】には連番画像の一番大きい数字が入ります。0~40までキャプチャした時は40です。
    - 【動画の書き出し先フォルダパス】にmovie.mp4という動画ファイルが書き出されます。
        - Read and Writeなフォルダを指定してください。
    - 【ffmpegのパス】はffmpegのアプリのパスです。
        - ビルド後も変わらないように注意。

#AForge.Video.VFW（失敗..）
[AForge.NET](http://www.aforgenet.com/framework/)のAForge.Video.VFW.dllプラグインを使用して.aviの動画を作成する。
Macでやってみたがavifil32.dllがないと怒られ調べるとWindowsのdllなのでMacOSは非対応かもしれない。
もしかしたらAForge.NETの公開しているソースをMac環境でビルドすることで使用可能になる可能性はある

###検証結果
| 動作環境 | 動作 | 画質 | 速度 |
|:-:|:-:|:-:|:-:|
| Windows | ... | ... | ... |
| Mac | 失敗 |   |   |
| Android | ... | ... | ... |
| iOS | ... | ... | ... |

###検証内容
- プラグインをプロジェクトで使用可能にするまで
    - トップページの画面左のリストにDownloadsがあるのでクリック<br>
      <img width="300" alt="toppage" src="https://qiita-image-store.s3.amazonaws.com/0/82938/228813a0-dc8c-9c81-0e41-29689a118099.png">

    - [ Download Full ZIP Archive ]をクリックしてダウンロード<br>
      <img width="300" alt="downloads" src="https://qiita-image-store.s3.amazonaws.com/0/82938/f8fd24f1-a693-9c8a-842f-1ec34d688522.png">

    - ダウンロードしたZIPを解凍してReleaseフォルダ内のAForge.dll、AForge.Video.dll、AForge.Video.VFW.dllをプロジェクトの/Assets/Plugins/にコピーする。

- プラグインを使用して実装したAForgeMovieConvertクラス
    - AVIWriter()の部分でavifil32.dllがないとエラー
    - その先の処理については仮実装...

```csharp:AForgeMovieConvert.cs
using UnityEngine;
using AForge.Video.VFW;

public class AForgeMovieConvert : MonoBehaviour {
	
	public delegate void movieConvertCallback(bool success, string error);
	private movieConvertCallback convertCallback = null;

	public void movieConvert(string savePath, string sourcePath, int width, int height, int second, movieConvertCallback callback) {
		if (callback != null) {
			convertCallback = callback;
			if (savePath != null && savePath != "") {
				if (sourcePath != null && sourcePath != "") {
					if (width > 0) {
						if (height > 0) {
							if (second > 0) {
								var writer = new AVIWriter();
								writer.Codec = "MSVC"; //4文字のコーデック
								writer.FrameRate = 15; //FrameRate = sourcePath内の連番画像の枚数 / second(動画の表示秒数)
								writer.Open(savePath + "movie.avi", width, height); //savePathにmovie.aviを作成

								//作ったmovie.aviにフレーム画像を描画していく
								for (int i = 0; i < 10; i++) {
									//bitmapはsourcePath内の連番画像を使用する、現在は仮
									System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(width, height);
									writer.AddFrame (bitmap);
								}
								writer.Close();

								//ここで動画が作成完了、動画ファイルを開いたりしたいならClose後
							} else {
								convertCallback (false, "movie second is 0");
							}
						} else {
							convertCallback (false, "movie height is 0");
						}
					} else {
						convertCallback (false, "movie width is 0");
					}
				} else {
					convertCallback (false, "Notfound:sourcePath");
				}
			} else {
				convertCallback (false, "Notfound:savePath");
			}
		} else {
			Debug.Log ("callback not found");
		}
	}
}
```

- 説明
    - コーデックは動画の書き出し方法みたいなもの...[Video Codecs by FOURCC](http://www.fourcc.org/codecs.php)

#iOS - AVAssetWriter（失敗..）
<img width="30" alt="apple_logo" src="https://qiita-image-store.s3.amazonaws.com/0/82938/8dd9e7b6-9f70-8a74-7b14-da8f8ac3c428.png">　iOSに関してはネイティブでプラグインを書いてやろうとやってみました。

###検証結果
| 動作環境 | 動作 | 画質 | 速度 |
|:--|:-:|:-:|:-:|
| iOS | 失敗 |   |   |

結果としてはautoreleasepoolで囲っているfor文の中まではくるが動画生成終了のハンドラーが呼ばれず...

> **tkyaji様より助言 - 2016-11-17 00:05**
AVAssetWriter#finishWritingWithCompletionHandlerのコールバックは、
iOS8だとiOSのバグで呼ばれなかった気がします。（iOS7も？）
iOS9の場合は呼ばれたような。。。

###検証内容
```objc:iOSMovieConvert.mm
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

//指定したパスの画像をカメラロールに保存する。
extern "C" void _MovieConvert (const char* savePath, const char* sourcePath , const int width, const int height, const int second, const char* gameObjectName, const char* callbackMethodName)
{
    //charをNSStringに変換
    NSString *save_path = [NSString stringWithCString:savePath encoding:NSUTF8StringEncoding];
    NSString *source_path = [NSString stringWithCString:sourcePath encoding:NSUTF8StringEncoding];
    
    if (save_path && source_path && ![save_path isEqual:@""] && ![source_path isEqual:@""]) {
        //動画の素材にする連番画像の取得
        NSMutableArray *images = [[NSMutableArray alloc] init];
        for (int i = 0; true; i++) {
            //画像のパス生成
            NSString *imagePath = [NSString stringWithFormat:@"%@screenshot_%d.png", source_path, i];
            if (![[NSFileManager defaultManager] fileExistsAtPath:imagePath]) {
                NSData *image_data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imagePath]];
                if (image_data) {
                    UIImage *image = [UIImage imageWithData:image_data];
                    if (image) {
                        [images addObject:image];
                    } else {
                        break;
                    }
                } else {
                    break;
                }
            } else {
                break;
            }
        }
        
        if ([images count] > 0) {
            //保存先URLと動画の形式を指定(.mov)　AVFileTypeMPEG4 = .mp4
            NSURL *url = [NSURL fileURLWithPath:save_path];
            AVAssetWriter *videoWriter = [[AVAssetWriter alloc] initWithURL:url fileType:AVFileTypeQuickTimeMovie error:nil];
            
            //コーデックと動画描画サイズの指定
            NSDictionary *outputSettings = @{
                AVVideoCodecKey : AVVideoCodecH264,
                AVVideoWidthKey : @(width),
                AVVideoHeightKey: @(height)
            };
            AVAssetWriterInput *writerInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
                                                                                 outputSettings:outputSettings];
            [videoWriter addInput:writerInput];
            
            //動画の素材にする連番画像の設定
            NSDictionary *sourcePixelBufferAttributes = @{
                (NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32ARGB),
                (NSString *)kCVPixelBufferWidthKey: @(width),
                (NSString *)kCVPixelBufferHeightKey: @(height)
            };
            AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:writerInput
                                                                                                                             sourcePixelBufferAttributes:sourcePixelBufferAttributes];
            writerInput.expectsMediaDataInRealTime = YES;
            
            //生成開始できるか確認
            if ([videoWriter startWriting]) {
                //動画生成開始
                [videoWriter startSessionAtSourceTime:kCMTimeZero];
                
                //全画像をバッファに貯めこむ
                CVPixelBufferRef buffer = NULL;
                int frameCount = 0;
                float durationForEachImage = [images count]/second; //画像1枚の表示時間
                int32_t fps = 24; //FPS
                for (UIImage *image in images) {
                    @autoreleasepool {
                        if (!adaptor.assetWriterInput.readyForMoreMediaData) {
                            break;
                        }
                        
                        // 動画の時間を生成（その画像の表示する時間。開始時点と表示時間を渡す）
                        CMTime frameTime = CMTimeMake((int64_t)frameCount * fps * durationForEachImage, fps);
                        
                        // CGImageからバッファを生成（後述）
                        NSDictionary *options = @{ (NSString *)kCVPixelBufferCGImageCompatibilityKey: @YES,
                                                   (NSString *)kCVPixelBufferCGBitmapContextCompatibilityKey: @YES, };
                        
                        buffer = NULL;
                        
                        CGFloat width  = CGImageGetWidth(image.CGImage);
                        CGFloat height = CGImageGetHeight(image.CGImage);
                        CVPixelBufferCreate(kCFAllocatorDefault,
                                            width,
                                            height,
                                            kCVPixelFormatType_32ARGB,
                                            (__bridge CFDictionaryRef)options,
                                            &buffer);
                        
                        CVPixelBufferLockBaseAddress(buffer, 0);
                        void *pxdata = CVPixelBufferGetBaseAddress(buffer);
                        
                        size_t bitsPerComponent       = 8;
                        size_t bytesPerRow            = 4 * width;
                        CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
                        CGContextRef context = CGBitmapContextCreate(pxdata,
                                                                     width,
                                                                     height,
                                                                     bitsPerComponent,
                                                                     bytesPerRow,
                                                                     rgbColorSpace,
                                                                     (CGBitmapInfo)kCGImageAlphaNoneSkipFirst);
                        
                        CGContextDrawImage(context, CGRectMake(0, 0, width, height), image.CGImage);
                        CGColorSpaceRelease(rgbColorSpace);
                        CGContextRelease(context);
                        
                        CVPixelBufferUnlockBaseAddress(buffer, 0);
                        
                        // 生成したバッファを追加
                        if (![adaptor appendPixelBuffer:buffer withPresentationTime:frameTime]) {
                            // Error!
                        }
                        
                        if (buffer) {
                            CVBufferRelease(buffer);
                        }
                        
                        frameCount++;
                    }
                }
                
                // 動画生成終了
                [writerInput markAsFinished];
                [videoWriter endSessionAtSourceTime:CMTimeMake((int64_t)(frameCount - 1) * fps * durationForEachImage, fps)];
                [videoWriter finishWritingWithCompletionHandler:^{
                    //動画の生成が終了したことをUnityへ報告
                    UnitySendMessage(gameObjectName, callbackMethodName, nil);
                }];
                
                // 後片付け
                CVPixelBufferPoolRelease(adaptor.pixelBufferPool);
            } else {
                //動画の生成ができない状態をUnityへ報告
                UnitySendMessage(gameObjectName, callbackMethodName, "AVAssetWriter startWriting is false");
            }
        } else {
            //動画の素材にする連番画像が取得できない状態をUnityへ報告
            UnitySendMessage(gameObjectName, callbackMethodName, "Source Images is Not found");
        }
    } else {
        //savePath,sourcePathが不正であることをUnityへ報告
        UnitySendMessage(gameObjectName, callbackMethodName, "savePath,sourcePath is null");
    }
}
```

- 説明
    - _MovieConvertメソッドの引数説明
        - savePath…動画を保存するファイルパス
            - Read and Writeなフォルダを指定してください。
        - sourcePath…screenshot_0.pngから始まる連番画像のパス
            - Read and Writeなフォルダを指定してください。
        - width…動画の横サイズ
        - height…動画の縦サイズ
        - second…動画の再生秒数
        - gameObjectName…この処理を呼び出したScriptのGameObjectの名前
        - callbackMethodName…処理後にUnityにコールバックするメソッド名
    - AVFileTypeQuickTimeMovieで動画の書き出し形式を指定
    - UnitySendMessageやネイティブプラグインの使用方法は以下を参照
        - [Unityでネイティブの機能を呼び出して処理後コールバックを受け取る](http://qiita.com/nanokanato/items/d6ebfcd69086c3fc5e79)

#OpenCV（現在検証中）
<img width="30" alt="ffmpeg_logo" src="https://qiita-image-store.s3.amazonaws.com/0/82938/cce384f5-2aa9-dc10-88d3-be226278c35f.png">　OpenCVにはVideoWriterというクラスがあるらしく連番画像を動画にできることを知りました。

ここではC++のOpenCVを元に作られたAsset、OpenCVForUnityを使って実装していきます。
OpenCVForUnityの開発開始までの手順に関しては[OpenCVForUnityをImportするまで](http://qiita.com/nanokanato/items/acc7fa6927b5a71755c1)をご覧ください。
OpenCVForUnityはMacやWindow,Android,iOSにも対応しているので有力です。

| 動作環境 | 動作 | 画質 | 速度 |
|:-:|:-:|:-:|:-:|
| Windows | ... | ... | ... |
| Mac | 検証中 | ... | ... |
| Android | ... | ... | ... |
| iOS | ... | ... | ... |

```csharp:CVVideoConvert.cs
using UnityEngine;
using System;
using OpenCVForUnity;

public class CVVideoConvert : MonoBehaviour {
    検証中...お待ちください。
}
```