Screen Recorder Android Plugin in Unity
Asked Answered
A

2

6

I'm developing an Unity-Android Plugin to record game screen and create a mp4 video file.I follow to Android Breakout game recorder patch sample in this site : http://bigflake.com/mediacodec/.
First, I create my CustomUnityPlayer class that extends UnityPlayer class and override onDrawFrame method.Here is my CustomUnityPlayer class code :

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);
}
}

And then, i create a CustomUnityPlayerActivity to call this class

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);
  }
}

My problem is that a video file is created successful but my game can't render anything.I read on Android Media Codec sample site and recognize that each frame would be render twice (once for the display, once for the video) but I can't do this in Unity.Whenever I try to call super.onDrawFrame(gl) twice in onDrawFrame method , my game will be crashed.

Any solution for my problem? Any help will be greatly appreciated!

Thanks and best regard!

Huy Tran

Anticipant answered 26/11, 2013 at 8:11 Comment(5)
FWIW, there are two basic approaches: (1) render some frames twice (as done in Breakout), (2) render to an offscreen FBO and blit twice. Depending on the complexity of the scene, one may be cheaper than the other. If you're using GLES 3, there's a trick to avoid one copy in approach #2. In any event, the real trick is integration with Unity.Wellpreserved
Thanks for your reply.Now my problem is integration with Unity.Can you explain more clear about approach #2.I tried with approach #1 as described above but no succeed!Anticipant
FWIW, all three approaches are demonstrated in Grafika (github.com/google/grafika). See the "Record GL app" activity.Wellpreserved
I want to record my app's screen, firstly i use glreadpixel, but too slow, how is your workSuited
Finally I use (2) approach: render to an offscreen FBO and use its binding texture to blit twice : once for video surface, once for screen. It works but need to be improve performanceAnticipant
A
6

Finally I use a FrameBufferObject (FBO) to render offscreen and get its binding texture to blit twice:

  • Render to video surface
  • Redraw to device screen

You can find more detail about this solution by reference to my another question use FBO to record Unity gamescreen

Anticipant answered 2/9, 2014 at 6:10 Comment(0)
I
4

Kamcord plug-in can help you : http://www.kamcord.com/

Impresario answered 26/11, 2013 at 8:28 Comment(3)
Kamcord currently only works on Nexus 4 and 7 devices running Android 4.3 Jelly Bean and Unity 4.2.I want to develop this plugin for more other devicesAnticipant
Since Kamcord is integrated with the game engine, this is the best approach from a technical standpoint, but I'd recommend a careful review of the legal terms... you consent to their monitoring of your apps, and you may not be able to post your videos ("End Users will only be able to access Kamcord Videos through distribution channels determined by Kamcord in its sole discretion").Wellpreserved
As I said above, Kamcord currently only supports for Nexus devices.In my case, I want my plugin can work with many other devicesAnticipant

© 2022 - 2024 — McMap. All rights reserved.