Android-плагин для записи экрана в Unity

Я разрабатываю плагин Unity-Android для записи экрана игры и создания видеофайла mp4. Я следую образцу патча для записи игр Android Breakout на этом сайте: ="noreferrer">http://bigflake.com/mediacodec/.
Сначала я создаю свой класс CustomUnityPlayer, который расширяет класс UnityPlayer и переопределяет метод onDrawFrame. Вот код моего класса CustomUnityPlayer:

package com.example.screenrecorder;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.ContextWrapper;
import android.opengl.EGL14;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
import android.opengl.EGLSurface;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.Matrix;
import android.util.Log;


import com.unity3d.player.*;

public class CustomUnityPlayer extends UnityPlayer implements GLSurfaceView.Renderer {

public static final String TAG = "ScreenRecord";
public static final boolean EXTRA_CHECK = true;         // enable additional assertions
private GameRecorder recorder;
static final float mProjectionMatrix[] = new float[16];
private final float mSavedMatrix[] = new float[16];
private EGLDisplay mSavedEglDisplay;
private EGLSurface mSavedEglDrawSurface;
private EGLSurface mSavedEglReadSurface;
private EGLContext mSavedEglContext;

// Frame counter, used for reducing recorder frame rate.
private int mFrameCount;

static final float ARENA_WIDTH = 768.0f;
static final float ARENA_HEIGHT = 1024.0f;

private int mViewportWidth, mViewportHeight;
private int mViewportXoff, mViewportYoff;



private final float[] mViewMatrix = new float[16];
private final float[] mRotationMatrix = new float[16];
private float mAngle;

public CustomUnityPlayer(ContextWrapper context) {
    // TODO Auto-generated constructor stub
    super(context);
    this.recorder = GameRecorder.getInstance();
}

 private boolean recordThisFrame() {
        final int TARGET_FPS = 30;

        mFrameCount ++;
        switch (TARGET_FPS) {
        case 60:
            return true;
        case 30:
            return (mFrameCount & 0x01) == 0;
        case 24:
            // want 2 out of every 5 frames
            int mod = mFrameCount % 5;
            return mod == 0 || mod == 2;
        default:
            return true;
        }
    }

public void onDrawFrame(GL10 gl){

    //record this frame
    if (this.recorder.isRecording() && this.recordThisFrame()) {    

        saveRenderState();

        // switch to recorder state
        this.recorder.makeCurrent();
        super.onDrawFrame(gl);
        this.recorder.getProjectionMatrix(mProjectionMatrix);
        this.recorder.setViewport();

        this.recorder.swapBuffers();    

        restoreRenderState();
    }
}

public void onSurfaceCreated(GL10 paramGL10, EGLConfig paramEGLConfig){
    // now repeat it for the game recorder
    if (this.recorder.isRecording()) {
        Log.d(TAG, "configuring GL for recorder");
        saveRenderState();
        this.recorder.firstTimeSetup();
        super.onSurfaceCreated(paramGL10, paramEGLConfig);
        this.recorder.makeCurrent();
        //glSetup();
        restoreRenderState();

        mFrameCount = 0;
    }

    if (EXTRA_CHECK) Util.checkGlError("onSurfaceCreated end");
}

public void onSurfaceChanged(GL10 unused, int width, int height) {
    /*
     * We want the viewport to be proportional to the arena size.  That way a 10x10
     * object in arena coordinates will look square on the screen, and our round ball
     * will look round.
     *
     * If we wanted to fill the entire screen with our game, we would want to adjust the
     * size of the arena itself, not just stretch it to fit the boundaries.  This can have
     * subtle effects on gameplay, e.g. the time it takes the ball to travel from the top
     * to the bottom of the screen will be different on a device with a 16:9 display than on
     * a 4:3 display.  Other games might address this differently, e.g. a side-scroller
     * could display a bit more of the level on the left and right.
     *
     * We do want to fill as much space as we can, so we should either be pressed up against
     * the left/right edges or top/bottom.
     *
     * Our game plays best in portrait mode.  We could force the app to run in portrait
     * mode (by setting a value in AndroidManifest, or by setting the projection to rotate
     * the world to match the longest screen dimension), but that's annoying, especially
     * on devices that don't rotate easily (e.g. plasma TVs).
     */

    super.onSurfaceChanged(unused, width, height);
    if (EXTRA_CHECK) Util.checkGlError("onSurfaceChanged start");

    float arenaRatio = ARENA_HEIGHT / ARENA_WIDTH;
    int x, y, viewWidth, viewHeight;

    if (height > (int) (width * arenaRatio)) {
        // limited by narrow width; restrict height
        viewWidth = width;
        viewHeight = (int) (width * arenaRatio);
    } else {
        // limited by short height; restrict width
        viewHeight = height;
        viewWidth = (int) (height / arenaRatio);
    }
    x = (width - viewWidth) / 2;
    y = (height - viewHeight) / 2;

    Log.d(TAG, "onSurfaceChanged w=" + width + " h=" + height);
    Log.d(TAG, " --> x=" + x + " y=" + y + " gw=" + viewWidth + " gh=" + viewHeight);

    GLES20.glViewport(x, y, viewWidth, viewHeight);

    mViewportXoff = x;
    mViewportYoff = y;
    mViewportWidth = viewWidth;
    mViewportHeight = viewHeight;


    // Create an orthographic projection that maps the desired arena size to the viewport
    // dimensions.
    //
    // If we reversed {0, ARENA_HEIGHT} to {ARENA_HEIGHT, 0}, we'd have (0,0) in the
    // upper-left corner instead of the bottom left, which is more familiar for 2D
    // graphics work.  It might cause brain ache if we want to mix in 3D elements though.
    Matrix.orthoM(mProjectionMatrix, 0,  0, ARENA_WIDTH,
            0, ARENA_HEIGHT,  -1, 1);

    Log.d(TAG, "onSurfaceChangedEnd 1 w=" + width + " h=" + height);

    if (EXTRA_CHECK) Util.checkGlError("onSurfaceChanged end");
    Log.d(TAG, "onSurfaceEnded w=" + width + " h=" + height);
}


public void pause(){
    super.pause();
    this.recorder.gamePaused();
}



/**
 * Saves the current projection matrix and EGL state.
 */
public void saveRenderState() {
    System.arraycopy(mProjectionMatrix, 0, mSavedMatrix, 0, mProjectionMatrix.length);
    mSavedEglDisplay = EGL14.eglGetCurrentDisplay();
    mSavedEglDrawSurface = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW);
    mSavedEglReadSurface = EGL14.eglGetCurrentSurface(EGL14.EGL_READ);
    mSavedEglContext = EGL14.eglGetCurrentContext();
}

/**
 * Saves the current projection matrix and EGL state.
 */
public void restoreRenderState() {
    // switch back to previous state
    if (!EGL14.eglMakeCurrent(mSavedEglDisplay, mSavedEglDrawSurface, mSavedEglReadSurface,
            mSavedEglContext)) {
        throw new RuntimeException("eglMakeCurrent failed");
    }
    System.arraycopy(mSavedMatrix, 0, mProjectionMatrix, 0, mProjectionMatrix.length);
}
}

Затем я создаю CustomUnityPlayerActivity для вызова этого класса.

package com.example.screenrecorder;

import android.content.res.Configuration;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.Window;

import com.unity3d.player.UnityPlayerActivity;

public class CustomUnityActivity extends UnityPlayerActivity {

private CustomUnityPlayer mUnityPlayer;
private GameRecorder mRecorder;

@Override
protected void onCreate(Bundle paramBundle){
    Log.e("ScreenRecord","oncreate");
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    super.onCreate(paramBundle);
    this.mUnityPlayer = new CustomUnityPlayer(this);
    if (this.mUnityPlayer.getSettings().getBoolean("hide_status_bar", true))
      getWindow().setFlags(1024, 1024);

    int glesMode = mUnityPlayer.getSettings().getInt("gles_mode", 1);
    boolean trueColor8888 = false;
    mUnityPlayer.init(glesMode, trueColor8888);

    View playerView = mUnityPlayer.getView();
    setContentView(playerView);
    playerView.requestFocus();

    this.mRecorder = GameRecorder.getInstance();
    this.mRecorder.prepareEncoder(this);
}

public void beginRecord(){
    Log.e("ScreenRecord","start record");


    this.mUnityPlayer.saveRenderState();
    this.mRecorder.firstTimeSetup();
    this.mRecorder.setStartRecord(true);
    this.mRecorder.makeCurrent();
    this.mUnityPlayer.restoreRenderState();
}

public void endRecord(){
    Log.e("ScreenRecord","end record");
    this.mRecorder.endRecord();
    this.mRecorder.setStartRecord(false);
    //this.mTransView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}

public boolean isRecording(){
    return this.mRecorder.isRecording();
}

protected void onDestroy()
  {
    super.onDestroy();
    this.mUnityPlayer.quit();
  }

  protected void onPause()
  {
    super.onPause();
    this.mUnityPlayer.pause();
  }

  protected void onResume()
  {
    super.onResume();
    this.mUnityPlayer.resume();
  }

  public void onConfigurationChanged(Configuration paramConfiguration)
  {
    super.onConfigurationChanged(paramConfiguration);
    this.mUnityPlayer.configurationChanged(paramConfiguration);
  }

  public void onWindowFocusChanged(boolean paramBoolean)
  {
    super.onWindowFocusChanged(paramBoolean);
    this.mUnityPlayer.windowFocusChanged(paramBoolean);
  }

  public boolean onKeyDown(int paramInt, KeyEvent paramKeyEvent)
  {
    return this.mUnityPlayer.onKeyDown(paramInt, paramKeyEvent);
  }

  public boolean onKeyUp(int paramInt, KeyEvent paramKeyEvent)
  {
    return this.mUnityPlayer.onKeyUp(paramInt, paramKeyEvent);
  }
}

Моя проблема в том, что видеофайл успешно создан, но моя игра ничего не может отобразить. Я прочитал пример Android Media Codec site и узнаю, что каждый кадр будет отображаться дважды (один раз для дисплея, один раз для видео), но я не могу сделать это в Unity. Всякий раз, когда я пытаюсь дважды вызвать super.onDrawFrame(gl) в методе onDrawFrame, мой игра вылетит.

Есть решение моей проблемы? Мы будем очень признательны за любую помощь!

Спасибо и всего наилучшего!

Хай Тран


person knighthedspi    schedule 26.11.2013    source источник
comment
FWIW, есть два основных подхода: (1) рендерить некоторые кадры дважды (как это делается в Breakout), (2) рендерить в закадровый FBO и дважды блицировать. В зависимости от сложности сцены один может быть дешевле другого. Если вы используете GLES 3, есть хитрость, позволяющая избежать одной копии в подходе № 2. В любом случае, настоящая фишка — это интеграция с Unity.   -  person fadden    schedule 26.11.2013
comment
Спасибо за ваш ответ. Теперь моя проблема связана с интеграцией с Unity. Можете ли вы более четко объяснить подход № 2. Я пробовал подход № 1, как описано выше, но безуспешно!   -  person knighthedspi    schedule 27.11.2013
comment
Между прочим, все три подхода продемонстрированы в Grafika (github.com/google/grafika). См. действие приложения Record GL.   -  person fadden    schedule 13.03.2014
comment
Я хочу записать экран своего приложения, во-первых, я использую greadpixel, но слишком медленно, как ваша работа   -  person CPT    schedule 01.09.2014
comment
Наконец, я использую (2) подход: рендеринг на закадровый FBO и использование его текстуры привязки для блитирования дважды: один раз для видеоповерхности, один раз для экрана. Это работает, но нужно улучшить производительность   -  person knighthedspi    schedule 01.09.2014


Ответы (2)


Наконец, я использую FrameBufferObject (FBO) для рендеринга вне экрана и дважды заставляю его текстуру привязки:

  • Рендеринг на поверхность видео
  • Перерисовать на экран устройства

Вы можете найти более подробную информацию об этом решении со ссылкой на мой другой вопрос использовать FBO для записи Экран игры Unity

person knighthedspi    schedule 02.09.2014

Плагин Kamcord может помочь вам: http://www.kamcord.com/

person nguoitotkhomaisao    schedule 26.11.2013
comment
В настоящее время Kamcord работает только на устройствах Nexus 4 и 7 под управлением Android 4.3 Jelly Bean и Unity 4.2. Я хочу разработать этот плагин для других устройств. - person knighthedspi; 26.11.2013
comment
Поскольку Kamcord интегрирован с игровым движком, это лучший подход с технической точки зрения, но я бы порекомендовал внимательно изучить юридические условия... вы даете согласие на мониторинг ваших приложений, и вы не можете публиковать свои видеоролики (Конечные пользователи смогут получить доступ к видеороликам Kamcord только через каналы распространения, определенные Kamcord по своему усмотрению). - person fadden; 26.11.2013
comment
Как я сказал выше, в настоящее время Kamcord поддерживает только устройства Nexus. В моем случае я хочу, чтобы мой плагин мог работать со многими другими устройствами. - person knighthedspi; 27.11.2013