company_banner

Захват видео в Unity3d с помощью Intel INDE Media Pack для Android

    imageВ одном из комментариев к статье про захват видео в OpenGL приложениях была упомянута возможность захвата видео в приложениях созданных с помощью Unity3d. Нас заинтересовала эта тема, на самом деле — почему только «чистые» OpenGL приложения, если многие разработчики используют для создания игр различные библиотеки и фреймворки? Сегодня мы рады представить готовое решение – захват видео в приложениях написанных с использованием Unity3d под Android.

    Бонус!

    По мотивам этой статьи вы не только научитесь встраивать захват видео в Unity3d, но и создавать Unity плагины под Android.

    Далее будут рассмотрены два варианта реализации захвата видео в Unity3d:

    1. Полноэкранный пост эффект. Способ будет работать только в Pro версии, при этом в видео не будет захватываться Unity GUI

    2. С помощью кадрового буфера (FrameBuffer). Будет работать для всех версий Unity3d, включая платную и бесплатную, объекты Unity GUI будут так же записываться в видео.

    Что нам понадобится


    • Unity3d версии 4.3 Pro версия для первого и второго методов, либо бесплатная версия, для которой доступен только метод с кадровым буфером
    • Установленный Android SDK
    • Установленный Intel INDE Media Pack
    • Apache Ant (для сборки Unity плагина под Android)


    Создание проекта


    Откройте редактор Unity и создайте новый проект. В папке ассетов создайте папку Plugins, а в ней папку Android.

    В папке, в которую был установлен Intel INDE Media Pack for Android, из директории libs скопируйте два jar-файла (android-<версия>.jar и domain-<версия>.jar) в папку Android своего проекта.

    image

    В той же папке Android создайте новый файл с именем Capturing.java и скопируйте в него следующий код:

    Capturing.java

    package com.intel.inde.mp.samples.unity;
    
    import com.intel.inde.mp.android.graphics.FullFrameTexture;
    
    import android.os.Environment;
    import java.io.IOException;
    import java.io.File;
    
    public class Capturing
    {
    
        private static FullFrameTexture texture;
    	
        public Capturing()
        {
            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)
        {
            VideoCapture capture = VideoCapture.getInstance();
    
            synchronized (capture) 
            {
                try 
                {
                    capture.start(videoPath);
                } 
                catch (IOException e) 
                {
                }
            }
        }
    
        // Вызывается для каждого захватываемого кадра
        public void captureFrame(int textureID)
        {
            VideoCapture capture = VideoCapture.getInstance();
    
            synchronized (capture) 
            {
                capture.beginCaptureFrame();
                texture.draw(textureID);
                capture.endCaptureFrame();
            }
        }
    
        // Остановка процесса захвата видео
        public void stopCapturing()
        {
            VideoCapture capture = VideoCapture.getInstance();
    
            synchronized (capture) 
            {
                if (capture.isStarted()) 
                {
                    capture.stop();
                }
            }
        }
    }
    


    Добавьте еще один Java файл, в этот раз с именем VideoCapture.java:

    VideoCapture.java

    package com.intel.inde.mp.samples.unity;
    
    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 VideoCapture()
        {
        }
        
        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 static VideoCapture getInstance()
        {
            if (videoCapture == null) 
            {
                synchronized (syncObject) 
                {
                    if (videoCapture == null)
                    {
                        videoCapture = new VideoCapture();
                    }
                }
            }
            return videoCapture;
        }
    
        public void start(String videoPath) throws IOException
        {
            if (isStarted())
            {
                throw new IllegalStateException(TAG + " already started!");
            }
    
            capturer = new GLCapture(new AndroidMediaObjectFactory());
            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. Оно должно совпадать с именем в настройках проекта (Player Settings/Other Settings/Bundle identifier):

    image

    Более того, вы должны использовать то же имя в C#-скрипте для вызова Java-класса. Если все эти имена не совпадут, ваша игра упадет при старте.

    Добавьте в вашу сцену какое-либо динамичное содержимое. Так же вы можете интегрировать Intel INDE Media Pack for Android* с любым существующим проектом, а не создавать его с нуля. Но постарайтесь, чтобы в сцене было что-то динамичное. Иначе вам не слишком будет интересно смотреть на видео-ролики, в которых ничего не меняется.

    Теперь, как в любом другом Android приложении, мы должны настроить манифест. Создайте в папке /Plugins/Android файл AndroidManifest.xml и скопируйте в него содержимое:

    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"
    

    Имя пакета должно совпадать с тем, что вы указывали ранее.

    Теперь у нас есть всё необходимое. Так как Unity не сможет скомпилировать самостоятельно наши Java-файлы мы создадим Ant-скрипт.

    Примечание: если вы используете другие классы и библиотеки, вы должны изменить ваш Ant-скрипт соответствующим образом (подробнее об этом в документации).

    Следующий Ant-скрипт предназначен только для этого урока. Создайте в папке /Plugins/Android/ файл build.xml:

    build.xml

    
    <?xml version="1.0" encoding="UTF-8"?>
    <project name="UnityCapturing">
        <!-- Change this in order to match your configuration -->
        <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"/>
        <!-- Source directory -->
        <property name="source.dir" value="\ProjectPath\Assets\Plugins\Android" />
        <!-- Output directory for .class files-->
        <property name="output.dir" value="\ProjectPath\Assets\Plugins\Android\classes"/>
        <!-- Name of the jar to be created. Please note that the name should match the name of the class and the name
        placed in the AndroidManifest.xml-->
        <property name="output.jarfile" value="Capturing.jar"/>
          <!-- Creates the output directories if they don't exist yet. -->
        <target name="-dirs"  depends="message">
            <echo>Creating output directory: ${output.dir} </echo>
            <mkdir dir="${output.dir}" />
        </target>
       <!-- Compiles this project's .java files into .class files. -->
        <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.0.903.jar"/>
    				<pathelement location="${source.dir}\android-1.0.903.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>
    


    Обратите внимание на пути source.dir, output.dir и, конечно же, на имя выходного jar-файла output.jarfile.

    В режиме командной строки перейдите в папке проекта /Plugins/Android и запустите процесс построения плагина

    ant build-jar clean-post-jar
    

    Если вы все сделали как описывалось выше, то через несколько секунд вы получите сообщение о том, что сборка успешно завершена!

    image

    На входе в папке должен появится новый файл Capturing.jar, содержащий код нашего плагина.

    Плагин готов, осталось внести необходимые изменения в код Unity3d, первым делом создадим обертку, связывающую Unity и наш Android плагин. Для этого создайте в проекте файл Capture.cs

    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 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) 
            {
    			// Получаем указатель на наш класс
    			IntPtr classID = AndroidJNI.FindClass("com/intel/inde/mp/samples/unity/Capturing");
    
    			// Ищем конструктор
    			constructorMethodID = AndroidJNI.GetMethodID(classID, "<init>", "()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);
    
    			// Создаем объект
    			IntPtr local_capturingObject = AndroidJNI.NewObject(classID, constructorMethodID, args);
    			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);
    	}
    }
    


    Назначьте этот скрипт главной камере. Перед захватом видео вы должны сконфигурировать формат видео. Вы можете это сделать прямо в редакторе, изменив соответствующие параметры (videoWidth, videoHeight и т.д.)

    Методы Start(), StartCapturing() и StopCapturing() достаточно тривиальны и представляют из себя обертки для вызова кода плагина из Unity.

    Более интересен метод OnRenderImage(). Он вызывается после того как весь рендеринг уже закончен, непосредственно перед выводом результата на экран. Входное изображение содержится в текстуре src, результат мы должны записать в текстуру dest.

    Этот механизм позволяет модифицировать финальную картинку, накладывая различные эффекты, но это вне зоны наших интересов, нас интересует картинка как есть. Для захвата видео мы должны скопировать финальное изображение в видео. Для этого мы передаем Id текстуры объекту Capturing с помощью вызова captureFrame() и передачей Id текстуры в качестве входного параметра.

    Для отрисовки на экране просто копируем src в dest:

    Graphics.Blit(src, dest);
    

    Для удобства давайте создадим кнопку, с помощью которой мы будем включать, выключать запись видео из интерфейса игры.

    Для этого создадим объект GUI и закрепим за ним обработчик. Обработчик будет находится в файле CaptureGUI.cs

    CaptureGUI.cs

    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 экземпляром класс Capture.

    При нажатии на объект будет запускаться, останавливаться процесс захвата видео, результат будет сохраняться в папке /mnt/sdcard/DCIM/.

    Как я уже говорил ранее этот способ будет работать только в Pro версии (в бесплатной версии нельзя использовать OnRenderImage() и вызвать Graphics.Blit), еще одна особенность – финальное видео не будет содержать объектов Unity GUI. Данные ограничения устраняются способом номер два – с использованием FrameBuffer.

    Захват видео с использованием кадрового буфера


    Внесем изменения в файл Capturing.java, для этого просто заменим его содержимое

    Capturing.java

    package com.intel.inde.mp.samples.unity;
    
    import com.intel.inde.mp.android.graphics.FullFrameTexture;
    import com.intel.inde.mp.android.graphics.FrameBuffer;
    
    import android.os.Environment;
    
    import java.io.IOException;
    import java.io.File;
    
    public class Capturing
    {
        private static FullFrameTexture texture;
        private FrameBuffer frameBuffer;
    	
        public Capturing(int width, int height)
        {
    	    frameBuffer = new FrameBuffer();
        	frameBuffer.create(width, height);
    
    	    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)
        {
            VideoCapture capture = VideoCapture.getInstance();
    
            synchronized (capture) 
            {
                try 
                {
                    capture.start(videoPath);
                } 
                catch (IOException e) 
                {
                }
            }
        }
    	
        public void beginCaptureFrame()
        {
        	frameBuffer.bind();
        }
    	
        public void captureFrame(int textureID)
        {
            VideoCapture capture = VideoCapture.getInstance();
    
            synchronized (capture) 
            {
                capture.beginCaptureFrame();
                texture.draw(textureID);
                capture.endCaptureFrame();
            }
        }
    	
        public void endCaptureFrame()
        {
        	frameBuffer.unbind();
    
        	int textureID = frameBuffer.getTexture();
    
        	captureFrame(textureID);
        	texture.draw(textureID);
        }
    
        public void stopCapturing()
        {
            VideoCapture capture = VideoCapture.getInstance();
    
            synchronized (capture) 
            {
                if (capture.isStarted()) 
                {
                    capture.stop();
                }
            }
        }
    }
    


    Как вы можете заметить, изменений не так много. Главное из них – появление нового объекта

    FrameBuffer frameBuffer;
    

    Конструктор теперь принимает в качестве параметров ширину и высоту кадра, это требуется для создания FrameBuffer’а нужного размера.

    Появились три новых публичных метода: frameBufferTexture(), beginCaptureFrame() и endCaptureFrame(). Их значение станет более ясным, когда мы перейдем к коду на C#.

    Файл VideoCapture.java мы оставляем без изменений.

    Далее необходимо построить Android плагин, о том, как это делается мы разобрали выше.

    Теперь мы можем переключиться на Unity. Откройте скрипт Capture.cs и замените его содержимое:

    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 bool finalizeFrame = false;
    	private Texture2D texture = null;
    	
    	private static IntPtr constructorMethodID = IntPtr.Zero;
    	private static IntPtr initCapturingMethodID = IntPtr.Zero;
    	private static IntPtr startCapturingMethodID = IntPtr.Zero;
    	private static IntPtr beginCaptureFrameMethodID = IntPtr.Zero;
    	private static IntPtr endCaptureFrameMethodID = IntPtr.Zero;
    	private static IntPtr stopCapturingMethodID = IntPtr.Zero;
    
    	private static IntPtr getDirectoryDCIMMethodID = IntPtr.Zero;
    
    	private IntPtr capturingObject = IntPtr.Zero;
    
    	void Start()
    	{
    		if (!Application.isEditor) 
            {
    			// Получаем указатель на наш класс
    			IntPtr classID = AndroidJNI.FindClass("com/intel/inde/mp/samples/unity/Capturing ");
    
    			// Ищем конструктор
    			constructorMethodID = AndroidJNI.GetMethodID(classID, "<init>", "(II)V");
    
    			// Регистрируем методы, реализованные классом
    			initCapturingMethodID = AndroidJNI.GetMethodID(classID, "initCapturing", "(IIII)V");
    			startCapturingMethodID = AndroidJNI.GetMethodID(classID, "startCapturing", "(Ljava/lang/String;)V");
    			beginCaptureFrameMethodID = AndroidJNI.GetMethodID(classID, "beginCaptureFrame", "()V");
    			endCaptureFrameMethodID = AndroidJNI.GetMethodID(classID, "endCaptureFrame", "()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 = new jvalue[2];
    
    			constructorParameters[0].i = Screen.width;
    			constructorParameters[1].i = Screen.height;
    
    			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 OnPreRender()
    	{
    		if (inProgress && Time.time > nextCapture) 
            {
    			finalizeFrame = true;
    			nextCapture += 1.0f / videoFrameRate;
    			BeginCaptureFrame();
    		}
    	}
    
    	public IEnumerator OnPostRender()
    	{
    		if (finalizeFrame) 
            {
    			finalizeFrame = false;
    			yield return new WaitForEndOfFrame();
    			EndCaptureFrame();
    		} 
            else 
            {
    			yield return null;
    		}
    	}
    
    	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 BeginCaptureFrame()
    	{
    		if (capturingObject == IntPtr.Zero)
    		{
                return;
            }
    
    		jvalue[] args = new jvalue[0];
    		AndroidJNI.CallVoidMethod(capturingObject, beginCaptureFrameMethodID, args);
    	}
    
    	private void EndCaptureFrame()
    	{
    		if (capturingObject == IntPtr.Zero)
    		{
                return;
            }
    
    		jvalue[] args = new jvalue[0];
    		AndroidJNI.CallVoidMethod(capturingObject, endCaptureFrameMethodID, args);
    	}
    
    	public void StopCapturing()
    	{
    		inProgress = false;
    
    		if (capturingObject == IntPtr.Zero)
    		{
                return;
            }
    
    		jvalue[] args = new jvalue[0];
    		AndroidJNI.CallVoidMethod(capturingObject, stopCapturingMethodID, args);
    	}
    }
    


    В этом коде у нас получилось гораздо больше изменений, но логика работы осталось простой. Сначала мы передаем размеры кадра в конструктор Capturing. Обратите внимание на новую сигнатуру конструктора — (II)V. На стороне Java мы создаем объект FrameBuffer и передаем ему указанные параметры.

    Метод OnPreRender() вызывается перед тем как камера начинает рендеринг сцены. Именно здесь мы переключаемся на наш FrameBuffer. Таким образом, весь рендеринг производится на текстуру, закрепленную за FrameBuffer.

    Метод OnPostRender() вызывается после окончания рендеринга. Мы ждем конца кадра, отключаем FrameBuffer и копируем текстуру прямо на экран средствами Media Pack (смотрите метод endCaptureFrame() класса Capturing.java).

    Производительность


    Часто разработчики спрашивают — насколько захват видео сказывается на производительности, как «просядет» FPS. Результат всегда зависит от конкретного приложения, сложности сцены и устройства, на котором запущено приложение.

    Чтобы у вас было средство оценки производительности давайте добавим простой счетчик FPS. Для этого добавьте на сцену объект Unity GUI и закрепите за ним следующий код:

    FPS.cs

    using UnityEngine;
    using System.Collections;
    
    public class FPSCounter : MonoBehaviour
    {
    	public float updateRate = 4.0f; // 4 updates per sec.
    
    	private int frameCount = 0;
    	private float nextUpdate = 0.0f;
    	private float fps = 0.0f;
    	private GUIStyle style = new GUIStyle();
    
    	void Start()
    	{
    		style.fontSize = 48;
    		style.normal.textColor = Color.white;
    
    		nextUpdate = Time.time;
    	}
    
    	void Update()
    	{
    		frameCount++;
    
    		if (Time.time > nextUpdate) 
            {
    			nextUpdate += 1.0f / updateRate;
    			fps = frameCount * updateRate;
    			frameCount = 0;
    		}
    	}
    
    	void OnGUI()
    	{
    		GUI.Label(new Rect(10, 110, 300, 100), "FPS: " + fps, style);
    	}
    }
    


    На этом можно считать нашу работу законченной, запускайте проект, эксперементируйте с записью. Если у вас появятся вопросы по интеграции с Unity3d или о работе с Intel INDE Media Pack – с радостью ответим на них в комментариях.
    • +13
    • 9.5k
    • 4
    Intel
    140.73
    Company
    Share post

    Comments 4

      0
      Стоит отметить, что метод применим только для приложений заточенных под захват экрана.
      Если стоит задача просто получить видео, например, для рекламных целей, то связываться с софтовыми решениями не имеет смысла.
      Гораздо проще и лучше купить плату видеозахвата за 80$ и в итоге получить качественное видео без проседания FPS.
      НИ в коем случае не против вашего продукта. Просто его применение очень нишевое. Врядли это реально нужно большому количеству Unity разработчиков.
        0
        Если цель сделать видео для рекламы, заливки на ютуб то, в случае Android, проще использовать ADB.

        Про нишевое — не согласен. Почему бы не дать пользователю возможность шарить свой игровой процесс в сетях, в обмен получая новый канал для продвижения? На мой взгляд потенциал у этой модели есть.
          +1
          Своей статистики пока нет, но вот разработчики Kamcord пишут

          Over the last two years, users have shared an incredible amount of content with Kamcord; over 4.5 million videos to date and 1 million in the last month alone.

          по моему хорошее начало.

          0
          День добрый. Скажите, при данном методе запись звука происходит с внешнего микрофона. Возможно ли записывать звук только из приложения?

          Only users with full accounts can post comments. Log in, please.