UnityでOpneCVを利用した動画再生をしてみた

#はじめに

**※記事の情報を「OpenCV for Unity 2.3.5」に対応した内容に更新しました。**


~~Unityでは動画ファイルを再生する機能として「[MovieTexture](http://docs.unity3d.com/ja/current/Manual/class-MovieTexture.html)」が用意されています。
動画ファイルを通常のTextureと同じように扱えるようになるため、映像をモデルに貼り付けて画面内に配置することが可能になります。
しかし、この機能はデスクトップ環境しかサポートしておらず、モバイル環境では使用できません。~~
※Unity2018.3からMovieTextureクラスは廃止されました。

**Unityの動画再生はUnity5.6から追加されたVideoPlayerコンポーネントを使う必要があります。（モバイル環境での再生も対応しています）**

Unityでは他にもiPhone/Androidのようなモバイル環境で動画再生が可能な、「[Handheld.PlayFullScreenMovie](http://docs.unity3d.com/ja/current/ScriptReference/Handheld.PlayFullScreenMovie.html)」という機能も存在しますが、全画面再生しかできないので使い勝手が非常に悪いです。(一旦、画面が切り替わって動画がフルスクリーン再生される方式。Unity側の処理が中断されるため、エンディングムービーの再生などの用途にしか使いどころが無い)

**今回の記事では「[OpenCV for Unity](https://assetstore.unity.com/packages/tools/integration/opencv-for-unity-21088)」を使用して動画再生をしてみたいと思います。**

※「OpenCV for Unity」の基本的な使い方とセットアップ方法はこちらの記事をチェックしてください。
- [UnityでOpenCVを利用した顔検出・画像処理アプリ事始め](http://qiita.com/utibenkei/items/fb93623a7ccaabb342f2)
- [UnityでOpenCVを利用した顔検出・画像処理アプリ事始め2 サンプルコード詳解](http://qiita.com/utibenkei/items/290e83d2c50cdadbcb2d)


##OpneCVの動画再生API「VideoCapture」とは

**「[VideoCapture](http://enoxsoftware.github.io/OpenCVForUnity/doc/html/class_open_c_v_for_unity_1_1_video_capture.html)」はビデオファイルやカメラからキャプチャを行うためのクラスです。**

再生可能な動画ファイルの形式ですが、**OpenCV3.0 rcからは「motion jpeg video」(MJPEG)の再生機能が追加された**ようです。

> 3.0 rc
April, 2015
These are changes since 3.0 beta
Standalone motion jpeg codec has been added to opencv_videoio. It does not need ffmpeg or any other 3rd-party lib. According to our measurements, it's also much faster than ffmpeg, especially on ARM. For the decoder you should have JPEG support enabled (through built-in or external libjpeg). How to use it? To encode motion jpeg video, use .avi file extension and CV_FOURCC('M', 'J', 'P', 'G'). The decoder part has been verified on such streams (avi files with index, where each frame is encoded using baseline jpeg) and few random motion jpeg clips from net, but we have not tested it thoroughly.
[OpenCV Change Logs](http://code.opencv.org/projects/opencv/wiki/Changelog)より引用

上記の説明からMJPEG形式であればサードパーティー製のライブラリを必要としないので環境に左右されずに再生が可能だと解釈しました。(もちろん従来通りffmpegなどのライブラリがリンクされてる環境であれば、対応している多様な形式の動画ファイルを再生可能)

**とりあえず、マルチプラットフォームに対応していると思われるMJPEG形式の動画ファイルを軸にして再生できるか試してみます。**


##MJPG形式の動画ファイルの作成

OpenCVにサンプルとして同梱されている動画ファイル「768x576.avi」を[ffmpeg](http://blog.k-tai-douga.com/)を使用してMJPEG形式に変換します。

```
ffmpeg -i 768x576.avi -r 10 -s 768x576 -vcodec mjpeg -an 768x576_mjpeg.avi
```

(サイズ、フレームレートは変更せず、コーデック形式だけ変換しました)

`ffmpeg -i [filename]`で情報を見てみたらこんな感じ

```
Input #0, avi, from '768x576_mjpeg.avi':
  Metadata:
    encoder         : Lavf56.34.100
  Duration: 00:01:19.50, start: 0.000000, bitrate: 1564 kb/s
    Stream #0:0: Video: mjpeg (MJPG / 0x47504A4D), yuvj420p(pc, bt470bg/unknown/unknown), 768x576, 1563 kb/s, 10 fps, 10 tbr, 10 tbn, 10 tbc
```



##再生テスト用のコード

![QiitaOpenCV2_movie.jpg](https://qiita-image-store.s3.amazonaws.com/0/28558/ba9f33bd-485a-a70d-9736-e106dcdd92b4.jpeg)


```
using UnityEngine;
using System.Collections;
using OpenCVForUnity;
using System;

	/// <summary>
	/// VideoCapture sample.
	/// </summary>
	public class VideoCaptureFileNameSample : MonoBehaviour
    {
        //環境や形式によっては動画の情報が正しく取得できないことがあるので、直接指定する
        private int fps = 10;
        private double frameWidth = 768;
        private double frameHeight = 576;

        private VideoCapture capture;
        private Mat rgbaMat;
        private Texture2D texture;

        private bool didUpdateThisFrame = false;
        private bool isThreadRunning = false;
        private ThreadComm threadComm = new ThreadComm();



#if UNITY_PRO_LICENSE || ((UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR) || !(UNITY_4_5 || UNITY_4_6)

        // Use this for initialization
        void Start()
        {

            rgbaMat = new Mat();

            capture = new VideoCapture();

            //capture.open(Utils.getFilePath("768x576.avi"));
#if UNITY_IPHONE && !UNITY_EDITOR
            capture.open(Utils.getFilePath("768x576_h264.mov"));
#else
            capture.open(Utils.getFilePath("768x576_mjpeg.avi"));
#endif

            if (capture.isOpened())
            {
                Debug.Log("capture.isOpened(): true");
            }
            else
            {
                Debug.Log("capture.isOpened(): false");
                return;
            }

            //動画ファイルの情報を表示
            Debug.Log("format: " + capture.get(8));
            Debug.Log("preview format: " + capture.get(Videoio.CV_CAP_PROP_PREVIEW_FORMAT));
            Debug.Log("CAP_PROP_POS_MSEC: " + capture.get(Videoio.CAP_PROP_POS_MSEC));
            Debug.Log("CAP_PROP_POS_FRAMES: " + capture.get(Videoio.CAP_PROP_POS_FRAMES));
            Debug.Log("CAP_PROP_POS_AVI_RATIO: " + capture.get(Videoio.CAP_PROP_POS_AVI_RATIO));
            Debug.Log("CAP_PROP_FRAME_COUNT: " + capture.get(Videoio.CAP_PROP_FRAME_COUNT));
            Debug.Log("CAP_PROP_FPS: " + capture.get(Videoio.CAP_PROP_FPS));
            Debug.Log("CAP_PROP_FRAME_WIDTH: " + capture.get(Videoio.CAP_PROP_FRAME_WIDTH));
            Debug.Log("CAP_PROP_FRAME_HEIGHT: " + capture.get(Videoio.CAP_PROP_FRAME_HEIGHT));
            double ex = capture.get(Videoio.CAP_PROP_FOURCC);
            //char EXT[] = {ex & 0XFF , (ex & 0XFF00) >> 8,(ex & 0XFF0000) >> 16,(ex & 0XFF000000) >> 24, 0};
            Debug.Log("CAP_PROP_FOURCC: " + (char)((int)ex & 0XFF) + (char)(((int)ex & 0XFF00) >> 8) + (char)(((int)ex & 0XFF0000) >> 16) + (char)(((int)ex & 0XFF000000) >> 24));


            //動画が画面いっぱいに表示されるように調整
            texture = new Texture2D((int)(frameWidth), (int)(frameHeight), TextureFormat.RGBA32, false);
            gameObject.GetComponent<Renderer>().material.mainTexture = texture;

#if (UNITY_ANDROID || UNITY_IPHONE || UNITY_WP_8_1) && !UNITY_EDITOR
            gameObject.transform.eulerAngles = new Vector3 (0, 0, -90);
#endif

            gameObject.transform.localScale = new Vector3((float)frameWidth, (float)frameHeight, 1);


#if (UNITY_ANDROID || UNITY_IPHONE || UNITY_WP_8_1) && !UNITY_EDITOR
			Camera.main.orthographicSize = (float)frameWidth / 2;
#else
            Camera.main.orthographicSize = (float)frameHeight / 2;
#endif

            //動画再生開始
            StartThread();

        }

        // Update is called once per frame
        void Update()
        {
            if (!didUpdateThisFrame) return;

            if (rgbaMat.total() > 0)
                 Utils.matToTexture2D(rgbaMat, texture);


            didUpdateThisFrame = false;
        }


        void OnDestroy()
        {

            StopThread();

            capture.release();

        }



        private void ThreadWorker()
        {
            if (isThreadRunning) return;

            isThreadRunning = true;

            threadComm.fps = fps;
            threadComm.frameCount = (int)capture.get(Videoio.CAP_PROP_FRAME_COUNT);

            StartCoroutine("CaptureCoroutine", threadComm);
        }

        
        IEnumerator CaptureCoroutine(System.Object o)
        {
            ThreadComm comm = o as ThreadComm;

            int msec = (int)(1000f / comm.fps);

            System.Diagnostics.Stopwatch stopWatch;

            while (true)
            {

                stopWatch = System.Diagnostics.Stopwatch.StartNew();

                if (capture.grab())
                {
                    //Loop play
                    if (capture.get(Videoio.CAP_PROP_POS_FRAMES) == comm.frameCount)
                        capture.set(Videoio.CAP_PROP_POS_FRAMES, 0);

                    capture.retrieve(rgbaMat, 0);
                    Imgproc.cvtColor(rgbaMat, rgbaMat, Imgproc.COLOR_BGR2RGB);

                    didUpdateThisFrame = true;
                }
                else
                {
                    break;
                }

                stopWatch.Stop();

                //FPSから計算したWaitを加える
                if (msec > (int)stopWatch.ElapsedMilliseconds)
                    yield return new WaitForSeconds((msec - stopWatch.ElapsedMilliseconds) / 1000f);
            }
        }


        public class ThreadComm : System.Object
        {
            public int fps;
            public int frameCount;
        }


        public void StartThread()
        {
            if (fps <= 0) return;

            ThreadWorker();
        }

        public void StopThread()
        {
            if (!isThreadRunning) return;



            StopCoroutine("CaptureCoroutine");
            isThreadRunning = false;

        }

#endif
}

```

当たり前ですが、「[OpenCV for Unity](https://assetstore.unity.com/packages/tools/integration/opencv-for-unity-21088)」のAssetの導入が必要です。
動画ファイルは「Assets/StreamingAssets」フォルダに入れてください。

とりあえず動画が再生できれば良いという方針で、コルーチンでFPSに応じたウェイトを加えながらフレームを更新しています。
一時停止やシーク機能はまだありません。とりあえずループ再生させています。


##各プラットフォームで再生テストをしてみた結果

以下の3種類の動画ファイルが再生できるかテストしました。

A. 768x576.avi (コーデック:div3 / コンテナ:avi)※OpenCVに同梱されている元ファイル
B. 768x576_mjpeg.avi (コーデック:mjpeg / コンテナ:avi)
**C. 768x576_mjpeg.mjpeg (コーデック:mjpeg / コンテナ:avi)※Bのファイル拡張子を.mjpegに変更したもの**
D. 768x576_h264.mov (コーデック:h264 / コンテナ:mov)[^1] 

| プラットフォーム | A.768x576.avi(div3/avi) | B.768x576_mjpeg.avi(mjpeg/avi) | C.768x576_mjpeg.mjpeg(mjpeg/avi) | D.768x576_h264.mov(h264/mov) |
|:------------|:------------:|:------------:|:--------------:|:--------------:|
| Windows      |     ○ ※1    |     ○     |     ○     |    ○ ※1    |
| Mac      |     ×     |     ○     |     ○     |    ○ ※2    |
| Android      |     ×     |     ○     |     ○     |    ×     |
| iPhone      |     ×     |     △ ※3    |     ○     |    ○     |
| WindowsStoreApp      |     ×     |     ○     |     ○     |    ×     |
| WebGL      |     ×     |     ○     |     ○     |    ×     |

※1 Windows版OpenCVに同梱されている「opencv_ffmpeg300_[64].dll」にパスが通っている場合のみ再生可能
※2 特に何もしていないけど再生できた（Macには元々QuickTimeが入っているためか?）
※3 OpenCVForUnity2.3.5で試したところ、再生に失敗しアプリが落ちる現象に遭遇しました。検証の結果、**拡張子を.aviから.mjepgに変更する**ことで再生可能になることがわかりました。（その対策のためか、OpenCVForUnityに同梱されいるサンプル動画の拡張子も変更されています。OpenCV側のバグみたいです）

**MJPEG形式でだいたいイケる！！（画質が劣化してファイルサイズが大きめだけど……）**
#まとめ

iPhoneだけは拡張子を変更する必要がありましたが、**UnityでOpenCVを使ってデスクトップとモバイル環境で動画を再生することに成功**しました。

**[次回の記事](http://qiita.com/utibenkei/items/a9c4e96a395666f9bf54)では動画再生を利用して、スマホVRアプリ(ハコスコ的な)の作成に挑戦してみたいと思います。**


# 関連記事
[UnityでOpenCVを利用した顔検出・画像処理アプリ事始め](http://qiita.com/utibenkei/items/fb93623a7ccaabb342f2)
[UnityでOpenCVを利用した顔検出・画像処理アプリ事始め2 サンプルコード詳解](http://qiita.com/utibenkei/items/290e83d2c50cdadbcb2d)
[「OpenCV for Unity」Assetのインポートサイズを削減する方法](http://qiita.com/utibenkei/items/eee141cd2f1656b19cc2)
UnityでOpneCVを利用した動画再生をしてみた
[UnityでOpneCVを利用したハコスコVR体験](http://qiita.com/utibenkei/items/a9c4e96a395666f9bf54)



[^1]: ffmpegを使用して「ffmpeg -i 768x576.avi -r 10 -s 768x576 -vcodec h264 -an 768x576_h264.mov」で変換したもの(iPhoneで再生可能な動画形式)