Unity中的屏幕录像机Android插件

时间:2013-11-26 08:11:23

标签: android opengl-es unity3d mediacodec screen-recording

我正在开发一个Unity-Android插件来记录游戏画面并创建一个mp4视频文件。我按照此站点中的Android Breakout游戏记录器补丁样本进行操作: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 sample网站上阅读并认识到每个帧都会渲染两次(一次用于显示,一次用于视频)但是我不能在Unity中做到这一点。每当我尝试在onDrawFrame方法中调用super.onDrawFrame(gl)两次时,我的游戏就会崩溃。

解决我的问题的任何方法?任何帮助将不胜感激!

谢谢,最好的关注!
Huy Tran

2 个答案:

答案 0 :(得分:4)

Kamcord插件可以帮助您:http://www.kamcord.com/

答案 1 :(得分:4)

最后我使用FrameBufferObject(FBO)来渲染屏幕并将其绑定纹理设置为blit两次:

  • 渲染到视频表面
  • 重绘到设备屏幕

您可以参考我的另一个问题use FBO to record Unity gamescreen

找到有关此解决方案的更多详细信息