LoginSignup
11
3

More than 1 year has passed since last update.

UnityでVideoPlayerを使わずにネイティブ実装で動画を再生する方法

Last updated at Posted at 2022-09-25

はじめに

業務で動画を扱う機会があり、AVProを利用して動画を再生していました。
AVProでどのように動画再生を実現しているのか気になり、動画再生サンプルを作成してみようと思い立ちました。
調べたところAVProではそれぞれのOSに備わっている動画再生機能を利用して動画を再生しているようです。
最初、ffmpegを使用してデコードすればそのような面倒な実装をしなくて済むのにと思いましたが、調べていると大きく2点ffmpegを利用することのデメリットがありました。
1点目がライセンスの問題で、ffmpegをアプリ内部に組み込んだ場合、アプリのソースコードも公開する必要がある問題です。
ffmpegをアプリ内に入れず、外部に持っていればこのような制限はないのですが、モバイルアプリでは難しいです。
2点目がアプリ内に組み込んだ場合、ライブラリの容量が大きいことです。
デスクトップでしたらあまり気にならない大きさかもしれませんが、モバイルアプリの場合、かなり大きい容量になってしまいます。

そこで今回、Windows(DirectX11)で動画再生する方法を調べてみることにしました。

サンプル

UiImage上で動画を再生するサンプルです。
MovieSample.gif

※今回のサンプルではwmv形式の動画のみ再生できることを確認しています。

以下のサイトからサンプル動画をお借りしました。

環境

Windows10 64bit
Unity2021.3.8f1 DirectX11
VisualStudio2019

使用する技術

Windowsで動画再生をするにあたって必要なライブラリなどを最初に紹介します。
UnityでC++ネイティブプラグインを作成する詳細は以前にまとめているので以下をご参照ください。

Unity Low-Level Native Plugin Interface

ネイティブプラグイン側で、Unityが生成したDirectX11デバイスやレンダリングイベントを受け取るために利用します。
インターフェースコードは以下のリポジトリで公開されていました。

Navite Plugin Interfaceの使用法は以下の記事を参考にさせて頂きました。

情報が少なかったのでとても助かりました。

DirectShow

Windowsで動画再生をするためのAPIです。
基本的な知識は〇×つくろーどっとコム様で勉強しました。

DirectX11

Unity側からテクスチャのポインタを受け取り、テクスチャの中身を更新するために使用します。
今回のサンプルではテクスチャを更新するだけなので、難しいことは特にしていません。

サンプルプロジェクトの解説

コード全体は以下のリポジトリをご参照ください。

デバッグログ連携

ネイティブプラグイン実装に当たって、Unity側でログの確認ができないと開発がうまく進められないためネイティブ側のコードから呼び出せるように対応しました。
しかし、別スレッドなどで呼び出したりするとUnityがクラッシュするのであくまで確認用でのみ使用したほうがよさそうです。

C#
TextureSample.cs

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void DebugLogDelegate(string str);
    private DebugLogDelegate debugLogFunc = Debug.Log;
    private DebugLogDelegate debugLogErrorFunc = Debug.LogError;

    [DllImport(DLL_NAME)]
    public static extern void SetDebugLogFunc(IntPtr ptr);
    
    [DllImport(DLL_NAME)]
    public static extern void SetDebugLogErrorFunc(IntPtr ptr);

    void Start()
    {
        // デバッグログ設定
        var logCallback = new DebugLogDelegate(debugLogFunc);
        var ptr = Marshal.GetFunctionPointerForDelegate(logCallback);
        SetDebugLogFunc(ptr);

        var logErrorCallback = new DebugLogDelegate(debugLogErrorFunc);
        var ptrError = Marshal.GetFunctionPointerForDelegate(logErrorCallback);
        SetDebugLogErrorFunc(ptrError);
    }

C++
main.cpp

    UNITY_INTERFACE_EXPORT void SetDebugLogFunc(global::DebugLogFuncType func) {
        global::debugLogFunc = func;
    }

    UNITY_INTERFACE_EXPORT void SetDebugLogErrorFunc(global::DebugLogFuncType func) {
        global::debugLogErrorFunc = func;
    }

global.h

namecpace global {
	using DebugLogFuncType = void(*)(const char*);
	DebugLogFuncType debugLogFunc = nullptr;
	DebugLogFuncType debugLogErrorFunc = nullptr;
	void DebugLog(const char* log) {
		if (debugLogFunc == nullptr) {
			return;
		}
		debugLogFunc(log);
	}

	void DebugLogError(const char* log) {
		if (debugLogErrorFunc == nullptr) {
			return;
		}
		debugLogErrorFunc(log);
	}
}

MoviePlayer生成

TextureSample.cs

        string moviePath = Path.Combine(Application.streamingAssetsPath, "TestMovie.wmv");
        moviePath = moviePath.Replace("/", "\\");
        _movieId = CreateMoviePlayer(moviePath);
        if (_movieId <= 0)
        {
            Debug.LogError("video init failed");
            return;
        }

ネイティブ側の関数(CreateMoviePlayer)を呼び出して、idを取得しています。
一度生成した後は、こちらのIDを使用してネイティブ関数を呼び出していくことになります。
C#側からはシンプルに動画パスを渡すだけで初期化完了になります。

Texture生成

TextureSample.cs

        // Texture作成
        var tex = new Texture2D((int)GetMovieWidth(_movieId), (int)GetMovieHeight(_movieId), TextureFormat.RGBA32, false);
        var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), Vector2.zero);
        _image.sprite = sprite;
        
        // Textureをセット
        SetMovieTexturePtr(_movieId, tex.GetNativeTexturePtr());

MoviePlayerを生成すると動画のサイズを取得できるようになります。
ネイティブ関数を通して幅と高さを取得しテクスチャを生成します。
テクスチャのフォーマットはRGBA32でUnity側とネイティブ側で決め打ちです。

Texture2Dの基底クラスTextureにはGetNativeTexturePtrという関数が用意されていまして、こちらでDirectX11のテクスチャのポインタを取得できます。
こちらの関数は、GraphicAPIを何に設定しているかで何のポインタが返ってくるか決まり、APIをOpenGLに設定していた場合はOpenGLのテクスチャのポインタが返ってきます。
今回のサンプルではDirectX11で決め打ちをしているため、それ以外のAPIを設定した場合は予期しないエラーになります。

レンダリングイベント発火コルーチン

TextureSample.cs

    // 描画コルーチン開始
    StartCoroutine(OnRender());

    IEnumerator OnRender()
    {
        while (true)
        {
            yield return new WaitForEndOfFrame();
            GL.IssuePluginEvent(GetRenderEventFunc(), 0);
        }
    }

ネイティブ側に描画イベントを伝えるための処理になります。
こちらのタイミング以外で、テクスチャの更新をかけると、Unityがクラッシュします。

動画再生開始

TextureSample.cs

        // 動画再生開始
        StartMovie(_movieId);

動画再生を開始します。
今回のサンプルでは、再生開始の機能しかありませんが、一時停止やシーク、再生速度変更も追加実装すれば可能です。

ネイティブ実装詳細

こちらからネイティブ側でどのように動画を再生しているか書いていきます。

MoviePlayer生成・登録

MoviePlayer生成Texture生成で呼び出している関数になります。

main.cpp

    UNITY_INTERFACE_EXPORT unsigned long UNITY_INTERFACE_API CreateMoviePlayer(char* moviePath) {
        auto moviePlayer = new MoviePlayer();
        if (FAILED(moviePlayer->Init(moviePath))) {
            return 0;
        }
        ULONG id = global::idIncrement++;
        global::moviePlayerMap[id] = moviePlayer;
        return id;
    }

    UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API SetMovieTexturePtr(unsigned long id, void* texturePtr) {
        global::movieTexturePtrMap[id] = texturePtr;
    }

サンプルでは一つの動画しか再生しませんが、複数の動画を再生できるように複数登録可能にしています。

破棄も忘れずに・・・

main.cpp

    UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API DisposeMoviePlayer(unsigned long id) {
        if (global::moviePlayerMap.count(id) != 0) {
            auto moviePlayer = global::moviePlayerMap[id];
            global::moviePlayerMap.erase(id);
            delete moviePlayer;
        }

        global::movieTexturePtrMap.erase(id);
    }

テクスチャ更新処理

レンダリングイベント発火コルーチンでイベントを受け取ってテクスチャの更新を書けます。

main.cpp

    void UNITY_INTERFACE_API OnRenderEvent(int eventId) {
        auto device = global::unity->Get<IUnityGraphicsD3D11>()->GetDevice();
        for (auto itr : global::moviePlayerMap) {
            if (global::movieTexturePtrMap.count(itr.first) == 0) {
                continue;
            }
            auto texture = (ID3D11Texture2D*)global::movieTexturePtrMap[itr.first];
            ID3D11DeviceContext* context;
            device->GetImmediateContext(&context);
            context->UpdateSubresource(texture, 0, nullptr, itr.second->GetMovieBuffer(), itr.second->GetWidth() * 4, 0);
        }
    }

    UNITY_INTERFACE_EXPORT UnityRenderingEvent UNITY_INTERFACE_API GetRenderEventFunc() {
        return OnRenderEvent;
    }

MoviePlayerから取得したバッファーを使用して、テクスチャに更新をかけています。

itr.second->GetWidth() * 4

はバッファーのピッチで、1行のデータのバイト数を指定しています。
今回はRGBA32で指定していて、1ピクセルあたりのバイト数は4バイトになるので、ピッチは「動画の幅×4」になります。

テクスチャバッファー取得フィルター

DirectShowでは、フィルターという概念がありましてこちらで様々な拡張を行っていくスタイルが取れるので自由度が高いです。
が、情報が少ないので自由自在に拡張しようと思うと長い道のりになると思います。
例えば、DirectShowはデフォルトでmp4の入力に対応していませんが、フィルターを追加することで再生可能にできたり、動画の入力源をWebカメラにすることなどが可能です。
今回は、動画をWindow(ディスプレイ)に出力するのではなく、1フレーム毎メモリのバッファーを更新するフィルターを作成します。

BaseVideoRenderer

一からフィルターを作成するのは大変なので、マイクロソフトが用意したクラスを継承させて作成していきます。
サンプルプロジェクトには導入済みですが、こちらを利用するためにはマイクロソフトのサンプルをビルドして、libファイルとヘッダファイルをプロジェクトに導入する必要があります。

ヘッダファイルとlibファイルの置き場
SimpleMovieTexture\NativeProjects\WinSimpleMovieTexture\WinSimpleMovieTexture\Libs
├─include
│  └─basecclasses
│          amextra.h
│          amfilter.h
│          cache.h
│          checkbmi.h
│          combase.h
│          cprop.h
│          ctlutil.h
│          ddmm.h
│          dllsetup.h
│          dxmperf.h
│          fourcc.h
│          measure.h
│          msgthrd.h
│          mtype.h
│          outputq.h
│          perflog.h
│          perfstruct.h
│          pstream.h
│          pullpin.h
│          refclock.h
│          reftime.h
│          renbase.h
│          schedule.h
│          seekpt.h
│          source.h
│          streams.h
│          strmctl.h
│          sysclock.h
│          transfrm.h
│          transip.h
│          videoctl.h
│          vtrans.h
│          winctrl.h
│          winutil.h
│          wxdebug.h
│          wxlist.h
│          wxutil.h
│
└─x64
        strmbase.lib

MovieTexture

MovieTexture.h

class MovieTexture : public CBaseVideoRenderer 
{
	std::unique_ptr<BYTE[]> pTextureBuffer;
public:
	MovieTexture(LPUNKNOWN pUnk, HRESULT* phr);
	~MovieTexture();

	HRESULT CheckMediaType(const CMediaType* pmt);     // Format acceptable?
	HRESULT SetMediaType(const CMediaType* pmt);       // Video format notification
	HRESULT DoRenderSample(IMediaSample* pSample);     // New video sample
	BYTE* GetTextureBuffer() { return pTextureBuffer.get(); };

	LONG lVidWidth;
	LONG lVidHeight;
	LONG lVidPitch;
};

以下の関数はフィルター実行時に自動的に呼ばれる関数です。

関数名 用途
CheckMediaType フィルターに対応している動画かどうかチェック
SetMediaType 動画の情報を取得(幅・高さなど)+初期化処理
DoRenderSample ★重要★ 動画が1フレーム進むごとに呼ばれる関数

DoRenderSample

MovieTexture.cpp

HRESULT MovieTexture::DoRenderSample(IMediaSample* pSample) {
	BYTE* pBmpBuffer;
	BYTE* pTxtBuffer = pTextureBuffer.get();

	CheckPointer(pSample, E_POINTER);

	pSample->GetPointer(&pBmpBuffer);

	int texIndex = 0;
	int sampleIndex = 0;
	for (int h = 0; h < lVidHeight; h++) {
		for (int w = 0; w < lVidWidth; w++) {
			BYTE b = pBmpBuffer[sampleIndex++];
			BYTE g = pBmpBuffer[sampleIndex++];
			BYTE r = pBmpBuffer[sampleIndex++];
			pTextureBuffer[texIndex++] = r;
			pTextureBuffer[texIndex++] = g;
			pTextureBuffer[texIndex++] = b;
			pTextureBuffer[texIndex++] = UCHAR_MAX;
		}
	}

	return S_OK;
}

引数で渡されるIMediaSampleから現在のフレームのバッファーを取得できます。

pSample->GetPointer(&pBmpBuffer);

Unity側でUiImageとして表示するためには、RGBA32である必要がありますが、動画のフォーマットはRGB24になるため変換が必要になります。
愚直にループを回して1ピクセルずつ値をセットしていきます。

こちらで保持したバッファーを、描画時に参照しています。

テクスチャバッファー取得フィルター追加

MoviePlayer.cpp

CComPtr<IGraphBuilder>  m_pGB;
CComPtr<MovieTexture>    m_pRenderer;

{
    HRESULT hr = S_OK;
    MovieTexture* pCTR = 0;

	pCTR = new MovieTexture(NULL, &hr);
	if (FAILED(hr) || !pCTR)
		return E_FAIL;

	m_pRenderer = pCTR;
	if (FAILED(hr = m_pGB->AddFilter(m_pRenderer, L"TextureRenderer")))
		return hr;
}

DirectShowの初期化の詳細は省きます。
フィルターを追加するだけで、DirectShow側で自動的にフィルター同士をつないでくれるようです。

番外編(mp4を再生するためには)

導入方法が分かっていないのですが、一応紹介します。
以下のリポジトリでDirectShowのmp4フィルターが公開されています。

AVProはWindowsでmp4再生をサポートしており、こちらのフィルターを利用している記述がドキュメントにあるので、上手く導入できればmp4の再生も実現可能かと思われます。

最後に

こちらのサンプル完成に至るまで、情報収集と検証がかなり大変でした。
最初独自の動画再生パッケージを作成出来たら良いかなと思っていたのですが、素直にUnity標準のVideoPlayerや、AVProを利用したほうが良いです。
こちらのサンプルを作成するにあたって、Unityとネイティブの連携について知見を深められたので、結果的には勉強になったと感じます。

11
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
3