インテル® INDE Media for Mobile チュートリアル – Android* 上での Unity3d* アプリケーションのビデオ・キャプチャー

同カテゴリーの次の記事

インテル® INDE Media for Mobile チュートリアル - Android* 上での Unity3d* アプリケーションの高度なビデオ・キャプチャー

この記事は、インテル® デベロッパー・ゾーンに掲載されている「Intel® INDE Media for Mobile Tutorials – Video Capturing for Unity3d* Applications on Android*」の日本語参考訳です。


このチュートリアルは、インテル® INDE Media for Mobile を使用して、Android* 上の Unity* アプリケーションにビデオ・キャプチャー機能を追加する方法を説明します。

必要条件:

  • Unity* 4.3.0 のキャプチャーは、フルスクリーン・イメージの後処理効果として動作します。後処理効果は Unity Pro* でのみ利用できます。
  • Android* SDK

このチュートリアルでは、Android* 向けの Unity* プラグインを作成し、コンパイルします。早速、作業に取り掛かりましょう。

Unity* を起動し、新しいプロジェクトを作成します。Project の下に、/Plugins/ という名前のサブディレクトリーを作成し、その下に /Android/ という名前のサブディレクトリーを作成します。

https://www.izzz.us/article/intel-software-dev-products/intel-inde/ からインテル® INDE をダウンロードし、インストールします。インテル® INDE のインストール後に、Media for Mobile のダウンロードを選択し、インストールします。不明な点がある場合は、インテル® INDE のフォーラム (英語) を参照してください。

<Media for Mobile のインストール・フォルダー>/libs にある 2 つの jar ファイル (android-<version>.jar と domain-<version>.jar) を /Assets/Plugins/Android/ フォルダーにコピーします。

Assets/Plugins/Android フォルダー

コピー先のフォルダーに、次のコードを含む Capturing.java という名前の Java* ファイルを作成します。

package com.intel.inde.mp.samples.unity;

import com.intel.inde.mp.IProgressListener;
import com.intel.inde.mp.domain.Resolution;
import com.intel.inde.mp.android.graphics.FullFrameTexture;

import android.os.Environment;
import android.content.Context;

import java.io.IOException;
import java.io.File;

public class Capturing
{

    private static FullFrameTexture texture;
    private VideoCapture videoCapture;
    
    private IProgressListener progressListener = new IProgressListener() {
        @Override
        public void onMediaStart() {
        }

        @Override
        public void onMediaProgress(float progress) {
        }

        @Override
        public void onMediaDone() {
        }

        @Override
        public void onMediaPause() {
        }

        @Override
        public void onMediaStop() {
        }

        @Override
        public void onError(Exception exception) {
        }
    };
    
    public Capturing(Context context)
    {
        videoCapture = new VideoCapture(context, progressListener);
        
        texture = new FullFrameTexture();
    }

    public static String getDirectoryDCIM()
    {
        return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + File.separator;
    }

    public void initCapturing(int width, int height, int frameRate, 
        int bitRate)
    {
        VideoCapture.init(width, height, frameRate, bitRate);
    }

    public void startCapturing(String videoPath)
    {
        if (videoCapture == null) {
            return;
        }
        synchronized (videoCapture) {
            try {
                videoCapture.start(videoPath);
            } catch (IOException e) {
            }
        }
    }

    public void captureFrame(int textureID)
    {
        if (videoCapture == null) {
            return;
        }
        synchronized (videoCapture) {
            videoCapture.beginCaptureFrame();
            texture.draw(textureID);
            videoCapture.endCaptureFrame();
        }
    }

    public void stopCapturing()
    {
        if (videoCapture == null) {
            return;
        }
        synchronized (videoCapture) {
            if (videoCapture.isStarted()) {
                videoCapture.stop();
            }
        }
    }

}

同じフォルダーに、次のコードを含む VideoCapture.java という名前の別の Java* ファイルを作成します。

package com.intel.inde.mp.samples.unity;

import android.content.Context;
import com.intel.inde.mp.*;
import com.intel.inde.mp.android.AndroidMediaObjectFactory;
import com.intel.inde.mp.android.AudioFormatAndroid;
import com.intel.inde.mp.android.VideoFormatAndroid;

import java.io.IOException;

public class VideoCapture
{
    private static final String TAG = "VideoCapture";

    private static final String Codec = "video/avc";
    private static int IFrameInterval = 1;

    private static final Object syncObject = new Object();
    private static volatile VideoCapture videoCapture;

    private static VideoFormat videoFormat;
    private static int videoWidth;
    private static int videoHeight;
    private GLCapture capturer;

    private boolean isConfigured;
    private boolean isStarted;
    private long framesCaptured;
    private Context context;
    private IProgressListener progressListener;

    public VideoCapture(Context context, IProgressListener progressListener)
    {
        this.context = context;
        this.progressListener = progressListener;
    }
    
    public static void init(int width, int height, int frameRate, 
        int bitRate)
    {
        videoWidth = width;
        videoHeight = height;
        
        videoFormat = new VideoFormatAndroid(Codec, videoWidth, videoHeight);
        videoFormat.setVideoFrameRate(frameRate);
        videoFormat.setVideoBitRateInKBytes(bitRate);
        videoFormat.setVideoIFrameInterval(IFrameInterval);
    }

    public void start(String videoPath) throws IOException
    {
        if (isStarted())
            throw new IllegalStateException(TAG + " already started!");

        capturer = new GLCapture(new 
            AndroidMediaObjectFactory(context), progressListener);
        capturer.setTargetFile(videoPath);
        capturer.setTargetVideoFormat(videoFormat);

        AudioFormat audioFormat = 
            new AudioFormatAndroid("audio/mp4a-latm", 44100, 2);
        capturer.setTargetAudioFormat(audioFormat);

        capturer.start();

        isStarted = true;
        isConfigured = false;
        framesCaptured = 0;
    }    
    
    public void stop()
    {
        if (!isStarted())
            throw new IllegalStateException(TAG + 
                " not started or already stopped!");

        try {
            capturer.stop();
            isStarted = false;
        } catch (Exception ex) {
        }

        capturer = null;
        isConfigured = false;
    }

    private void configure()
    {
        if (isConfigured())
            return;

        try {
            capturer.setSurfaceSize(videoWidth, videoHeight);
            isConfigured = true;
        } catch (Exception ex) {
        }
    }

    public void beginCaptureFrame()
    {
        if (!isStarted())
            return;

        configure();
        if (!isConfigured())
            return;

        capturer.beginCaptureFrame();
    }

    public void endCaptureFrame()
    {
        if (!isStarted() || !isConfigured())
            return;

        capturer.endCaptureFrame();
        framesCaptured++;
    }

    public boolean isStarted()
    {
        return isStarted;
    }

    public boolean isConfigured()
    {
        return isConfigured;
    }

}

重要: ここでは、com.intel.inde.mp.samples.unity というパッケージ名を使用しています。このパッケージ名は、Unity* のプレーヤー設定にある Bundle Identifier と同じでなければなりません。

Player Settings / Other Settings / Bundle Identifier

また、C# スクリプトで前述の Java* クラスを呼び出す際にこの名前を使用する必要があります。名前が一致しない場合、VM はクラス定義を見つけることができず、起動時にクラッシュします。

テスト・アプリケーション用にいくつかの簡単な 3D 要素を設定する必要があります。もちろん、既存のプロジェクトにインテル® INDE Media for Mobile を統合することもできます。どちらの場合も、画面で何か動くものを用意します。

次に、ほかの Android* アプリケーションと同様にマニフェスト XML ファイルを設定する必要があります。マニフェスト・ファイルは、起動するアクティビティーとアクセス可能な関数をコンパイル時に指定します。ここでは、C:/Program Files (x86)/Unity/Editor/Data/PlaybackEngines/androidplayer にあるデフォルトの Unity* AndroidManifest.xml をベースにします。/Plugins/Android 以下に、次のコードを含む AndroidManifest.xml を作成します。

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.intel.inde.mp.samples.unity"
    android:installLocation="preferExternal"
    android:theme="@android:style/Theme.NoTitleBar"
    android:versionCode="1"
    android:versionName="1.0">
      
    <uses-sdk android:minSdkVersion="18" />
    
    <uses-permission android:name=
        "android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    
    <!-- マイクの許可 -->
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    
    <!-- OpenGL* ES 2.0 以上が必要 -->
    <uses-feature
        android:glEsVersion="0x00020000"
        android:required="true"/>
    
    <application
     android:icon="@drawable/app_icon"
        android:label="@string/app_name"
        android:debuggable="true">
        <activity android:name="com.unity3d.player.UnityPlayerNativeActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <meta-data android:name="unityplayer.UnityActivity" 
                android:value="true" />
            <meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" 
                android:value="false" />
        </activity>
    </application>

</manifest>

次の行が重要です。

package="com.intel.inde.mp.samples.unity"

これで、/Plugins/Android 以下に AndroidManifest.xml と Java* ファイルを作成できました。 javac でのコンパイルで classpaths などを含む長いコマンドを使用する代わりに、Apache Ant* スクリプトを使用します。Ant* を使用することで、フォルダー作成、.exe 呼び出し、クラス作成用のスクリプトを簡単に作成できます。また、Ant* スクリプトは Eclipse* にインポートすることもできます。

: ほかのクラスやライブラリーを使用している場合は、次の Ant* スクリプトを変更する必要があります (http://ant.apache.org/manual/ (英語) にあるドキュメントを参考にしてください)。この Ant* スクリプトは、このチュートリアル専用です。

/Plugins/Android/ 以下に、次のコードを含む build.xml ファイルを作成します。

<?xml version="1.0" encoding="UTF-8"?>
<project name="UnityCapturing">
    <!-- 使用する設定に合わせて変更してください -->
    <property name="sdk.dir" value="C:\Android\sdk"/>
    <property name="target" value="android-18"/>
    <property name="unity.androidplayer.jarfile" 
        value="C:\Program Files (x86)\Unity\Editor\Data\PlaybackEngines\androiddevelopmentplayer\bin\classes.jar"/>
    <!-- ソース・ディレクトリー -->
    <property name="source.dir" 
        value="\ProjectPath\Assets\Plugins\Android" />
    <!-- .class ファイルの出力ディレクトリー -->
    <property name="output.dir" 
        value="\ProjectPath\Assets\Plugins\Android\classes"/>
    <!-- 作成する jar ファイルの名前。クラスおよび AndroidManifest.xml 内の名前と
    一致していなければなりません。-->
    <property name="output.jarfile" value="Capturing.jar"/>
      <!-- 出力ディレクトリーがない場合は作成します -->
    <target name="-dirs"  depends="message">
        <echo>Creating output directory: ${output.dir} </echo>
        <mkdir dir="${output.dir}" />
    </target>
   <!-- このプロジェクトの .java ファイルを .class ファイルにコンパイルします -->
    <target name="compile" depends="-dirs"
        description="Compiles project's .java files into .class files">
        <javac encoding="ascii" target="1.6" debug="true"
            destdir="${output.dir}" verbose="${verbose}"
            includeantruntime="false">
            <src path="${source.dir}" />
            <classpath>
                <pathelement
                    location="${sdk.dir}\platforms\${target}\android.jar"/>
                <pathelement location="${source.dir}\domain-1.2.2415.jar"/>
                <pathelement location="${source.dir}\android-1.2.2415.jar"/>
                <pathelement location="${unity.androidplayer.jarfile}"/>
            </classpath>
        </javac>
    </target>
    <target name="build-jar" depends="compile">
        <zip zipfile="${output.jarfile}"
            basedir="${output.dir}" />
    </target>
    <target name="clean-post-jar">
         <echo>Removing post-build-jar-clean</echo>
         <delete dir="${output.dir}"/>
    </target>
    <target name="clean"
        description="Removes output files created by other targets.">
        <delete dir="${output.dir}" verbose="${verbose}" />
    </target>
    <target name="message">
     <echo>Android Ant Build for Unity Android Plugin</echo>
        <echo>   message:      Displays this message.</echo>
        <echo>   clean:     Removes output files created by other targets.
        </echo>
        <echo>   compile:   Compiles project's .java files into .class files.
        </echo>
        <echo>   build-jar: Compiles project's .class files into .jar file.
        </echo>
    </target>
</project>

2 つのパス (source.dir と output.dir) と出力 jar ファイル名 (output.jarfile) は、実際に使用するものに変更する必要があります。

Ant* がインストールされていない場合は、Apache Ant* の Web サイト (英語) から入手できます。インストール後、実行 PATH に追加してください。Ant* を呼び出す前に、JAVA_HOME 環境変数を宣言し、Java* Development Kit (JDK) がインストールされているパスを指定します。PATH に <ant_home>/bin を追加することも忘れないでください。

Windows* のコマンドプロンプト (cmd.exe) を起動し、現在のディレクトリーを /Plugins/Android に変更して、次のコマンドでビルドスクリプトを起動します。

ant build-jar clean-post-jar

数秒後、ビルドに成功したことを示すメッセージが表示されます。

Ant の出力

これで jar のコンパイルは完了です。ディレクトリーに Capturing.jar が作成されていることを確認します。

それでは、Unity* に移りましょう。次のコードを含む Capture.cs スクリプトを作成します。

using UnityEngine;
using System.Collections;
using System.IO;
using System;

[RequireComponent(typeof(Camera))]
public class Capture : MonoBehaviour
{
    public int videoWidth = 720;
    public int videoHeight = 1094;
    public int videoFrameRate = 30;
    public int videoBitRate = 3000;

    private string videoDir;
    public string fileName = "game_capturing-";
    
    private float nextCapture = 0.0f;
    public bool inProgress { get; private set; }

    private AndroidJavaObject playerActivityContext = null;
    
    private static IntPtr constructorMethodID = IntPtr.Zero;
    private static IntPtr initCapturingMethodID = IntPtr.Zero;
    private static IntPtr startCapturingMethodID = IntPtr.Zero;
    private static IntPtr captureFrameMethodID = IntPtr.Zero;
    private static IntPtr stopCapturingMethodID = IntPtr.Zero;

    private static IntPtr getDirectoryDCIMMethodID = IntPtr.Zero;

    private IntPtr capturingObject = IntPtr.Zero;

    void Start()
    {
        if (!Application.isEditor) {
            // 最初に現在のアクティビティー・コンテキストを取得します
            using (AndroidJavaClass jc = 
                new AndroidJavaClass("com.unity3d.player.UnityPlayer")) {
                    playerActivityContext =
                        jc.GetStatic<AndroidJavaObject>("currentActivity");
            }

            // クラスを探します
            IntPtr classID =
                AndroidJNI.FindClass("com/intel/inde/mp/samples/unity/Capturing");

            // クラスのコンストラクターを探します
            constructorMethodID = 
                AndroidJNI.GetMethodID(classID, "<init>", 
                   "(Landroid/content/Context;)V");

            // メソッドを登録します
            initCapturingMethodID = 
                AndroidJNI.GetMethodID(classID, "initCapturing", "(IIII)V");
            startCapturingMethodID = 
                AndroidJNI.GetMethodID(classID, "startCapturing", 
                    "(Ljava/lang/String;)V");
            captureFrameMethodID = 
                AndroidJNI.GetMethodID(classID, "captureFrame", "(I)V");
            stopCapturingMethodID = 
                AndroidJNI.GetMethodID(classID, "stopCapturing", "()V");

            // 静的メソッドを登録し呼び出します
            getDirectoryDCIMMethodID = 
                AndroidJNI.GetStaticMethodID(classID, "getDirectoryDCIM", 
                    "()Ljava/lang/String;");
            jvalue[] args = new jvalue[0];
            videoDir = 
                AndroidJNI.CallStaticStringMethod(classID, 
                    getDirectoryDCIMMethodID, args);

            // キャプチャー・オブジェクトを作成します
            jvalue[] constructorParameters = 
                AndroidJNIHelper.CreateJNIArgArray(new object [] 
                    { playerActivityContext });
            IntPtr local_capturingObject = 
                AndroidJNI.NewObject(classID, constructorMethodID, 
                    constructorParameters);
            if (local_capturingObject == IntPtr.Zero) {
                Debug.LogError("Can't create Capturing object");
                return;
            }

            // キャプチャー・オブジェクトのグローバル参照を保持します
            capturingObject = AndroidJNI.NewGlobalRef(local_capturingObject);
            AndroidJNI.DeleteLocalRef(local_capturingObject);

            AndroidJNI.DeleteLocalRef(classID);
        }
        inProgress = false;
        nextCapture = Time.time;
    }

    void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (inProgress && Time.time > nextCapture) {
            CaptureFrame(src.GetNativeTextureID());
            nextCapture += 1.0f / videoFrameRate;
        }
        Graphics.Blit(src, dest);
    }

    public void StartCapturing()
    {
        if (capturingObject == IntPtr.Zero)
            return;

        jvalue[] videoParameters =  new jvalue[4];
        videoParameters[0].i = videoWidth;
        videoParameters[1].i = videoHeight;
        videoParameters[2].i = videoFrameRate;
        videoParameters[3].i = videoBitRate;
        AndroidJNI.CallVoidMethod(capturingObject, initCapturingMethodID, 
            videoParameters);
        DateTime date = DateTime.Now;
        string fullFileName = 
            fileName + date.ToString("ddMMyy-hhmmss.fff") + ".mp4";
        jvalue[] args = new jvalue[1];
        args[0].l = AndroidJNI.NewStringUTF(videoDir + fullFileName);
        AndroidJNI.CallVoidMethod(capturingObject, 
            startCapturingMethodID, args);

        inProgress = true;
    }

    private void CaptureFrame(int textureID)
    {
        if (capturingObject == IntPtr.Zero)
            return;

        jvalue[] args = new jvalue[1];
        args[0].i = textureID;
        AndroidJNI.CallVoidMethod(capturingObject, 
            captureFrameMethodID, args);
    }

    public void StopCapturing()
    {
        inProgress = false;

        if (capturingObject == IntPtr.Zero)
            return;

        jvalue[] args = new jvalue[0];
        AndroidJNI.CallVoidMethod(capturingObject, 
            stopCapturingMethodID, args);
    }

}

このスクリプトをメインカメラに追加します。キャプチャーする前にビデオ・フォーマットを設定する必要があります。各パラメーターの役割は、その名前が示すとおりです。これらのパラメーターは、Unity* Editor GUI から変更できます。

ここでは、Start()StartCapturing()StopCapturing() メソッドの説明は省略します。Java* Native Interface (JNI) を使い慣れた皆さんはすでにご存知でしょう。ほかのメソッドを見てみましょう。 OnRenderImage() メソッドは、すべてのレンダリングがイメージの描画を完了した後に呼び出されます。入力イメージはソース・レンダリング・テクスチャーで、結果はデスティネーション・レンダリング・テクスチャーになります。シェーダーベースのフィルターを使用して最終イメージを処理し、変更できます。ここでは、単に Graphics.Blit() を呼び出してソーステクスチャーをそのままデスティネーション・レンダリング・テクスチャーにコピーします。その前に、ネイティブ (“ハードウェア”) テクスチャー・ハンドルを Capturing.java クラスの captureFrame() メソッドに渡しています。

StartCapturing() メソッドと StopCapturing() メソッドは public なので、ほかのスクリプトから呼び出すことができます。次のコードを含む CaptureGUI.cs という名前の C# スクリプトを作成します。

using UnityEngine;
using System.Collections;

public class CaptureGUI : MonoBehaviour
{
    public Capture capture;
        private GUIStyle style = new GUIStyle();

        void Start()
        {
            style.fontSize = 48;
            style.alignment = TextAnchor.MiddleCenter;
        }

        void OnGUI()
        {
            style.normal.textColor = 
                capture.inProgress ? Color.red : Color.green;
            if (GUI.Button(new Rect(10, 200, 350, 100), 
                capture.inProgress ? "[Stop Recording]" : 
                "[Start Recording]", style)) {
                    if (capture.inProgress) {
                        capture.StopCapturing();
                    } else {
                        capture.StartCapturing();
                    }
            }
        }
	
}

このスクリプトをシーンの任意のオブジェクトに追加します。Capture.cs インスタンスを public capture メンバーに割り当てることも忘れないでください。

Unity* アプリケーションにビデオ・キャプチャー機能を追加するのに必要な作業はこれだけです。それでは、Android* プラットフォーム向けにテスト・アプリケーションをビルドし実行してみましょう。キャプチャーしたビデオは、Android* デバイスの /mnt/sdcard/DCIM/ フォルダーに保存されます。Capturing.java と VideoCapture.java コードのロジックを理解するには、こちらのチュートリアルが役立ちます。

既知の問題:

  • このアプローチでは、Unity* GUI レイヤーはキャプチャーできません。OnPreRender() メソッドと OnPostRender() メソッドを使用する回避策がありますが、ドロップシャドウ、遅延シェーディング、フルスクリーンの後処理効果では動作しません。

関連記事