Захват видео в Unity3d с помощью Intel INDE Media Pack для Android
В одном из комментариев к статье про захват видео в 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 своего проекта.
В той же папке 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):
Более того, вы должны использовать то же имя в C#-скрипте для вызова Java-класса. Если все эти имена не совпадут, ваша игра упадет при старте.
Добавьте в вашу сцену какое-либо динамичное содержимое. Так же вы можете интегрировать Intel® INDE Media Pack for Android* с любым существующим проектом, а не создавать его с нуля. Но постарайтесь, чтобы в сцене было что-то динамичное. Иначе вам не слишком будет интересно смотреть на видео-ролики, в которых ничего не меняется.
Теперь, как в любом другом Android приложении, мы должны настроить манифест. Создайте в папке /Plugins/Android файл AndroidManifest.xml и скопируйте в него содержимое:
AndroidManifest.xml
Обратите внимание на строчку:
package=«com.intel.inde.mp.samples.unity» Имя пакета должно совпадать с тем, что вы указывали ранее.Теперь у нас есть всё необходимое. Так как Unity не сможет скомпилировать самостоятельно наши Java-файлы мы создадим Ant-скрипт.
Примечание: если вы используете другие классы и библиотеки, вы должны изменить ваш Ant-скрипт соответствующим образом (подробнее об этом в документации).
Следующий Ant-скрипт предназначен только для этого урока. Создайте в папке /Plugins/Android/ файл build.xml:
build.xml
В режиме командной строки перейдите в папке проекта /Plugins/Android и запустите процесс построения плагина
ant build-jar clean-post-jar Если вы все сделали как описывалось выше, то через несколько секунд вы получите сообщение о том, что сборка успешно завершена!
На входе в папке должен появится новый файл 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,»
// Регистрируем методы, реализованные классом 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,»
// Регистрируем методы, реализованные классом 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 — с радостью ответим на них в комментариях.