2015年7月27日 星期一

How to Access Android Camera in Unity (III): Customized camera plugin with preview window and UI on it

Overview

   This article will continue the last article and add functionalities such as the camera preview window and buttons to allow user to adjust camera parameters.
  When Unity call this plugin, instead of take a snapshot and leave, the process will stay in the Camera Setting (unless you press the "Return" button so that it'll return to game),  as shown below. In this moment, Unity is temporarily blocked: You can't hear any game sound (unless you invoke it by the android API here), or running game in the background.


Solution


1. At first we need to add one more function "DisplayCameraSettingActivity()" in AndroidCameraAPI.cs, so that it would like:
/*
 * This Class Call the Android Code to Take Picture.
 * Note: there should be a AndroidCameraAPI.jar in \Assets\Plugins\Android
 */
public class AndroidCameraAPI : MonoBehaviour
{
    private AndroidJavaObject androidCameraActivity;

    public void Awake()
    {
        AndroidJavaClass unity = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        this.androidCameraActivity = unity.GetStatic<AndroidJavaObject>("currentActivity");
    }

    public void TakePhotoFromAndroidAPI(string _pathToSavePhoto, string _fileNameOfSavedPhoto)
    {
        this.androidCameraActivity.Call("TakePhoto",
                                        _pathToSavePhoto, _fileNameOfSavedPhoto+".jpg");
    }

    public void DisplayCameraSettingActivity()
    {
        this.androidCameraActivity.Call("DisplayCameraSettingActivity");
    }
}


2. In the Entrance of Android API (the MainActivity.java):
import android.content.Intent;
import android.hardware.Camera;
import android.os.Bundle;
import android.util.Log;

import com.unity3d.player.UnityPlayerActivity;

public class MainActivity extends UnityPlayerActivity
{
    private Camera androidCamera = null;
    private Camera.Parameters userDefinedCameraParameters = null;

    @Override
    public void onCreate(Bundle _savedInstanceState)
    {
        super.onCreate(_savedInstanceState);

        openCameraSafely();
        setupCameraWithDefaultParameters();
    }
    private void openCameraSafely()
    {
        Log.i("Unity", "Open Camera Safely...");
        try
        {
            this.androidCamera = Camera.open();
        }
        catch(Exception cameraIOException)
        {
            ExceptionManager.SaveErrorCodeToFile(ExceptionManager.CAMERA_ERROR_LOG_FILE_NAME,
                                                "Cannot Connect to Camera,"
                                                + " please Check connection...\n"
                                                + "\tIf this doesn't work, please"
                                                + " Replug the Camera or Reboot this Machine.");

            this.finish();
        }
        Log.i("Unity", "Open Camera Success!");
    }
    private void setupCameraWithDefaultParameters()
    {
        try
        {
            this.userDefinedCameraParameters = this.androidCamera.getParameters();
            this.userDefinedCameraParameters.setExposureCompensation(CameraDataCenter.userDefinedCameraExposureLevel);
            this.userDefinedCameraParameters.setZoom(CameraDataCenter.userDefinedCameraZoomLevel);
            this.userDefinedCameraParameters.setPictureSize(CameraDataCenter.PHOTO_WIDTH,
                                                            CameraDataCenter.PHOTO_HEIGHT);
            this.androidCamera.setParameters(this.userDefinedCameraParameters);
        }
        catch(Exception exception)
        {
            ExceptionManager.SaveErrorCodeToFile(ExceptionManager.CAMERA_ERROR_LOG_FILE_NAME,
                                                "Can't get CameraParameters,"
                                                +" which means the camera might in bad status."
                                                +" Do you access the camera with multithread?");
        }
    }

    public void DisplayCameraSettingActivity()
    {
        Log.i("Unity", "Go to Camera Settings...");
        Intent intentOfCameraSetting = new Intent(this, CameraSettingActivity.class);
        this.androidCamera.release();
        startActivityForResult(intentOfCameraSetting,
                                CameraDataCenter.CAMERA_SETTING_REQUEST_CODE);
    }

    protected void onActivityResult(int _requestCode, int _resultCode, Intent _cameraSettingIntent)
    {
        Log.i("Unity", "MainActivity.onActivityResult().");
        if( (_requestCode == CameraDataCenter.CAMERA_SETTING_REQUEST_CODE)
            &&(_resultCode == RESULT_OK) )
        {
            this.userDefinedCameraParameters.setExposureCompensation(CameraDataCenter.userDefinedCameraExposureLevel);
            this.userDefinedCameraParameters.setZoom(CameraDataCenter.userDefinedCameraZoomLevel);
        }
        this.openCameraSafely();
    }


    public void TakePhoto(final String _targetPathToSavePhoto,
                          final String _targetPhotoFileName)
    {
        Log.i("Unity", "Take Photo...");
        if(this.userDefinedCameraParameters != null)
            this.androidCamera.setParameters(this.userDefinedCameraParameters);
        this.androidCamera.startPreview();

            Camera.PictureCallback savePictureCallBack = new SavePictureCallBack(_targetPathToSavePhoto, _targetPhotoFileName);
            this.androidCamera.takePicture(null, null, savePictureCallBack);

            Log.i("Unity", "Take Photo Success!");
    }

    @Override
    public void onDestroy()
    {
        Log.i("Unity", "CameraActivity is Destroyed!");
        if(this.androidCamera != null)
            this.androidCamera.release();

        super.onDestroy();
    }

}

    You can see that when you call DisplayCameraSettingActivity(), the process will change to CameraSettingActivity which will be demonstrate bellow.
    Note that When you move to another Activity, you should release the lock of android.camera by camera.release() since only one activity can hold the lock of android.camera.
   Also note that when the CameraSettingActivity return (when the user hit the "Return" button on the screen), the function onActivityResult() will be called. That is the time that we require the lock of android.camera back (so that the TakePhoto() can be called latter in Unity). Then the process will return to Unity and continue the game.

3. The CameraSettingActivity.java is shown below:
public class CameraSettingActivity extends Activity
{
    private Camera androidCamera;
    private Camera.Parameters userDefinedCameraParameters = null;
    private CameraPreviewer cameraPreviewer;
    private FrameLayout cameraPreviewFrame;

    private LinearLayout linearLayout;
    private TextView exposureLevelText;
    private TextView zoomLevelText;
    private Button returnButton;
    private final float UI_TEXT_SIZE = 32f;


    @Override
    protected void onCreate(Bundle _savedInstanceState)
    {
        super.onCreate(_savedInstanceState);

        initializeLayoutSetting();

        openCameraSafely();
        setupCameraWithDefaultParameters();
        initializeCameraPreview();

        createReturnButton();
        CreateExposureInspector();
        CreateZoomLevelInspector();
    }
    private void initializeLayoutSetting()
    {
        DisplayMetrics displayedScreen = new DisplayMetrics();
        this.getWindowManager().getDefaultDisplay().getMetrics(displayedScreen);

        this.requestWindowFeature(Window.FEATURE_NO_TITLE);
        this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                                  WindowManager.LayoutParams.FLAG_FULLSCREEN);

        this.linearLayout = new LinearLayout(this);
        LayoutParams parametersOfLinearLayout = new LayoutParams(LayoutParams.MATCH_PARENT,
                                                                 LayoutParams.MATCH_PARENT);

        setContentView(this.linearLayout, parametersOfLinearLayout);
    }
    private void openCameraSafely()
    {
        try
        {
            this.androidCamera = Camera.open();
        }
        catch(Exception cameraIOException)
        {
            ExceptionManager.SaveErrorCodeToFile(ExceptionManager.CAMERA_ERROR_LOG_FILE_NAME,
                                                "Cannot Connect to Camera,"
                                                + " please Check connection...\n"
                                                + "\tIf this doesn't work, please"
                                                + " Replug the Camera or Reboot this Machine.");

            this.finish();
        }
    }

    /*
     * UI Related...
     */
    private void createReturnButton()
    {
        this.returnButton = new Button(this);
        this.returnButton.setText("Return");
        this.returnButton.setTextSize(UI_TEXT_SIZE);
        this.returnButton.setOnClickListener(
                new Button.OnClickListener()
                {
                    @Override
                    public void onClick(View _view)
                    {
                        OnReturnButtonClick(_view);
                    }
                }
        );
        this.returnButton.setX(100);
        this.returnButton.setY(600);
        addContentView( this.returnButton, new LayoutParams(150, 75) );
    }

    private void CreateExposureInspector()
    {
        createIncreaseExposureButton(350, 75, 75, 65);
        createCurrentExposureText(500, 85, 400, 70);
        createDecreaseExposureButton(850, 75, 75, 65);
    }
    private void createIncreaseExposureButton(int _left, int _top, int _width, int _height)
    {
        Button increaseExposureButton = new Button(this);
        increaseExposureButton.setText("+");
        increaseExposureButton.setTextSize(UI_TEXT_SIZE);
        increaseExposureButton.setOnClickListener(
                new Button.OnClickListener()
                {
                    @Override
                    public void onClick(View _view)
                    {
                        OnIncreaseExposureButtonClick(_view);
                    }
                }
        );
        increaseExposureButton.setX(_left);
        increaseExposureButton.setY(_top);
        addContentView( increaseExposureButton, new LayoutParams(_width, _height) );
    }
    private void createCurrentExposureText(int _left, int _top, int _width, int _height)
    {
        this.exposureLevelText = new TextView(this);
        this.exposureLevelText.setTextSize(UI_TEXT_SIZE);
        updateExposureLevelTexts();
        this.exposureLevelText.setX(_left);
        this.exposureLevelText.setY(_top);
        addContentView(this.exposureLevelText, new LayoutParams(_width, _height));
    }
    private void createDecreaseExposureButton(int _left, int _top, int _width, int _height)
    {
        Button decreaseExposureButton = new Button(this);
        decreaseExposureButton.setText("-");
        decreaseExposureButton.setTextSize(UI_TEXT_SIZE);
        decreaseExposureButton.setOnClickListener(
                new Button.OnClickListener()
                {
                    @Override
                    public void onClick(View _view)
                    {
                        OnDecreaseExposureButtonClick(_view);
                    }
                }
        );
        decreaseExposureButton.setX(_left);
        decreaseExposureButton.setY(_top);
        addContentView( decreaseExposureButton, new LayoutParams(_width, _height) );
    }
    private void updateExposureLevelTexts()
    {
        int currentExposureLevel = CameraDataCenter.userDefinedCameraExposureLevel;

        if( currentExposureLevel > 0)
            this.exposureLevelText.setText("Exposure Level:  +"+currentExposureLevel);
        else
            this.exposureLevelText.setText("Exposure Level:  "+currentExposureLevel);
    }

    private void CreateZoomLevelInspector()
    {
        createIncreaseZoomLevelButton(350, 150, 75, 65);
        createCurrentZoomLevelText(500, 160, 400, 70);
        createDecreaseZoomLevelButton(850, 150, 75, 65);
    }
    private void createIncreaseZoomLevelButton(int _left, int _top, int _width, int _height)
    {
        Button increaseZoomLevelButton = new Button(this);
        increaseZoomLevelButton.setText("+");
        increaseZoomLevelButton.setTextSize(UI_TEXT_SIZE);
        increaseZoomLevelButton.setOnClickListener(
                new Button.OnClickListener()
                {
                    @Override
                    public void onClick(View _view)
                    {
                        OnIncreaseZoomLeveleButtonClick(_view);
                    }
                }
        );
        increaseZoomLevelButton.setX(_left);
        increaseZoomLevelButton.setY(_top);
        addContentView( increaseZoomLevelButton, new LayoutParams(_width, _height) );
    }
    private void createCurrentZoomLevelText(int _left, int _top, int _width, int _height)
    {
        this.zoomLevelText = new TextView(this);
        this.zoomLevelText.setTextSize(UI_TEXT_SIZE);
        updateZoomLevelTexts();
        this.zoomLevelText.setX(_left);
        this.zoomLevelText.setY(_top);
        addContentView(this.zoomLevelText, new LayoutParams(_width, _height));
    }
    private void createDecreaseZoomLevelButton(int _left, int _top, int _width, int _height)
    {
        Button decreaseZoomLevelButton = new Button(this);
        decreaseZoomLevelButton.setText("-");
        decreaseZoomLevelButton.setTextSize(UI_TEXT_SIZE);
        decreaseZoomLevelButton.setOnClickListener(
                new Button.OnClickListener()
                {
                    @Override
                    public void onClick(View _view)
                    {
                        OnDecreaseZoomLevelButtonClick(_view);
                    }
                }
        );
        decreaseZoomLevelButton.setX(_left);
        decreaseZoomLevelButton.setY(_top);
        addContentView( decreaseZoomLevelButton, new LayoutParams(_width, _height) );
    }
    private void updateZoomLevelTexts()
    {
        int currentZoomLevel = CameraDataCenter.userDefinedCameraZoomLevel;

        if( currentZoomLevel > 0)
            this.zoomLevelText.setText("Zoom Level:  +"+currentZoomLevel);
        else
            this.zoomLevelText.setText("Zoom Level:  "+currentZoomLevel);
    }

    /*
     * Camera Settings Related...
     */
    private void initializeCameraPreview()
    {
        this.cameraPreviewer = new CameraPreviewer(this, this.androidCamera);

        this.cameraPreviewFrame = new FrameLayout(this);
        addContentView(this.cameraPreviewFrame,
                       new LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)
                      );
        this.cameraPreviewFrame.addView(this.cameraPreviewer);
    }
    private void setupCameraWithDefaultParameters()
    {
        try
        {
            this.userDefinedCameraParameters = this.androidCamera.getParameters();
            this.userDefinedCameraParameters.setPictureSize(CameraDataCenter.PHOTO_WIDTH,
                                                            CameraDataCenter.PHOTO_HEIGHT);
            updateCameraExposureSettings(CameraDataCenter.userDefinedCameraExposureLevel);
            updateCameraZoomSettings(CameraDataCenter.userDefinedCameraZoomLevel);
        }
        catch(Exception exception)
        {
            ExceptionManager.SaveErrorCodeToFile(ExceptionManager.CAMERA_ERROR_LOG_FILE_NAME,
                                                 "Can't get CameraParameters,"
                                                +" which means the camera might in bad status."
                                                +" Do you access the camera with multithread?");
        }
    }
    private void updateCameraExposureSettings(int _userRequiredExposure)
    {
        try
        {
            CameraDataCenter.userDefinedCameraExposureLevel = _userRequiredExposure;
            this.userDefinedCameraParameters.setExposureCompensation(_userRequiredExposure);
            this.androidCamera.setParameters(userDefinedCameraParameters);
        }
        catch(Exception exception)
        {
            ExceptionManager.SaveErrorCodeToFile(ExceptionManager.CAMERA_ERROR_LOG_FILE_NAME,
                                                "Can't Set CameraParameters,"
                                                +"  Are there Invalid Camera Settings "
                                                +"(such as Invalid Picture Size, Zoom Level"
                                                +" or Exposure Level)?");
        }
    }
    private void updateCameraZoomSettings(int _userDefinedZoomLevel)
    {
        try
        {
            CameraDataCenter.userDefinedCameraZoomLevel = _userDefinedZoomLevel;
            this.userDefinedCameraParameters.setZoom(_userDefinedZoomLevel);
            this.androidCamera.setParameters(userDefinedCameraParameters);
        }
        catch(Exception exception)
        {
            ExceptionManager.SaveErrorCodeToFile(ExceptionManager.CAMERA_ERROR_LOG_FILE_NAME,
                                                "Can't Set CameraParameters,"
                                                +"  Are there Invalid Camera Settings "
                                                +"(such as Invalid Picture Size, Zoom Level"
                                                +" or Exposure Level)?");
        }
    }

    /*
     * Following is the Call Back Functions for Button Clicked.
     */
    public void OnIncreaseExposureButtonClick(View _increaseExposureButton)
    {
        int currentExposure = this.userDefinedCameraParameters.getExposureCompensation();
        ++currentExposure;
        int maxExposure = this.userDefinedCameraParameters.getMaxExposureCompensation();
        if( currentExposure > maxExposure )
            currentExposure = maxExposure;
        updateCameraExposureSettings(currentExposure);
        updateExposureLevelTexts();
    }
    public void OnDecreaseExposureButtonClick(View _decreaseExposureButton)
    {
        int currentExposure = this.userDefinedCameraParameters.getExposureCompensation();
        --currentExposure;
        int minExposure = this.userDefinedCameraParameters.getMinExposureCompensation();
        if( currentExposure < minExposure)
            currentExposure = minExposure;
        updateCameraExposureSettings(currentExposure);
        updateExposureLevelTexts();
    }
    public void OnIncreaseZoomLeveleButtonClick(View _increaseZoomButton)
    {
        int currentZoomLevel = this.userDefinedCameraParameters.getZoom();
        ++currentZoomLevel;
        int maxZoomLevel = this.userDefinedCameraParameters.getMaxZoom();
        if( currentZoomLevel > maxZoomLevel )
            currentZoomLevel = maxZoomLevel;
        updateCameraZoomSettings(currentZoomLevel);
        updateZoomLevelTexts();
    }
    public void OnDecreaseZoomLevelButtonClick(View _decreaseZoomeButton)
    {
        int currentZoomLevel = this.userDefinedCameraParameters.getZoom();
        --currentZoomLevel;
        int minZoomLevel = 0;
        if( currentZoomLevel < minZoomLevel)
            currentZoomLevel = minZoomLevel;
        updateCameraZoomSettings(currentZoomLevel);
        updateZoomLevelTexts();
    }

    public void OnReturnButtonClick(View _returnButton)
    {
        this.androidCamera.stopPreview();

        Intent returnIntent = new Intent();
        setResult(RESULT_OK, returnIntent);

        Log.i("Unity", "Release Camera...");
        this.androidCamera.release();
        this.finish();
    }

    @Override
    public void onDestroy()
    {
        Log.i("Unity", "CameraActivity is Destroyed!");
        if(this.androidCamera != null)
            this.androidCamera.release();

        super.onDestroy();
    }
}
   You can see that I create the buttons dynamically in onCreate(), instead of using the R.layout (which is the normal way to create UI widget in android), because there're some issue about the missing of  R.layout when the Unity use android plugins. I guess the reason might be that Unity will replace the R.layout itself... And this is the reason that I don't call the
    setContentView(R.layout.activity_main);
in the onCreate() as we normally do when writing "pure" android app.

4. Since you have two Activities now, you should also change your AndroidManifest.xml to:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.igs.dinosaur"
      android:versionCode="1"
      android:versionName="1.0">
    <uses-sdk android:minSdkVersion="9" />
 
 <uses-permission android:name="android.permission.CAMERA" />
 <uses-feature android:name="android.hardware.camera" />
 <uses-feature android:name="android.hardware.camera.autofocus" />
 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 
    <application
  android:icon="@drawable/app_icon"
  android:label="@string/app_name"
  android:debuggable="true">
  
        <activity android:name=".MainActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
  
  <activity android:name=".CameraSettingActivity">
  </activity>
  
    </application>
</manifest>

5. Finally, here is the CameraPreviewer that will be called by the CameraSettingActivity:
public class CameraPreviewer extends SurfaceView
                            implements SurfaceHolder.Callback
{
    private SurfaceHolder displayedSurface;
    private Camera currentCamera;

    public CameraPreviewer(Context _cameraContext, Camera _camera)
    {
        super(_cameraContext);
        this.currentCamera = _camera;
        this.displayedSurface = this.getHolder();
        this.displayedSurface.addCallback(this);

        //Following line might be required if you use the early version of android
        this.displayedSurface.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }

    @Override
    public void surfaceCreated(SurfaceHolder _surfaceHolder)
    {
        try
        {
            this.currentCamera.setPreviewDisplay(_surfaceHolder);
            this.currentCamera.startPreview();
        }
        catch (IOException exception)
        {
            ExceptionManager.SaveErrorCodeToFile(ExceptionManager.CAMERA_ERROR_LOG_FILE_NAME,
                                                 "Camera Preview failed, this might due to"
                                                 +" the SurfaceHolder cannot be obtained...");
        }
    }

    @Override
    public void surfaceChanged(SurfaceHolder _surfaceHolder, int _format, int _width, int _height)
    {
        this.Refresh();
    }

    public void Refresh()
    {
        stopPreviewBeforeRefresh();
        restartPreview();
    }
    private void stopPreviewBeforeRefresh()
    {
        try
        {
            this.currentCamera.stopPreview();
        }
        catch(Exception _exception)
        {
            /*
             * This Exception can be Ignore, since it
             * might be no preview before.
             */
        }
    }
    private void restartPreview()
    {
        try
        {
            this.currentCamera.setPreviewDisplay(this.displayedSurface);
            this.currentCamera.startPreview();
        }
        catch (IOException exception)
        {
            ExceptionManager.SaveErrorCodeToFile(ExceptionManager.CAMERA_ERROR_LOG_FILE_NAME,
                                                 "Camera Preview failed, this might due to"
                                                 + " the SurfaceHolder cannot be obtained...");
        }
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder _surfaceHolder) {}
}


Result

    Now you can adjust the camera parameters by click the buttons and preview the camera, as shown below.



Conclusion

    Here're the advantages and disadvantages of calling the camera in Android API compare to the Unity API:

Advantages:

     As mention in the last article, you can adjust many camera parameters in android API.

Disadvantages:

    When calling the android API, Unity will temporary stopped. So if you want to play music, display UI or detect IO, you should do it in Android API! You can't rely on the easy-to-use Unity anymore.

2016/6/2 updated:
   We finally solve the Unity temporary stopped problem by using the Android Service. The Service is like running things in another thread, but yet its display can cover the original APP.
   Therefore, we extend the Service that can display camera video. When we want to show the WebCam preview window, we call that Service. And it will display the camera video which will cover the display of our game but yet leave the game still running.
    This solution could also be used to solve the temporary stop problem when you want to apply Unity API to display movie in Android. You can extend the Service to display movie so that the game will still running when the movie is playing.


7 則留言:

  1. Can we get pixels from the camera, pass it into Unity, then display it by a Unity Texture?

    回覆刪除
    回覆
    1. Sorry, I have no idea. Finally, we abandon this approach. Instead, we change the camera device (so that its default exposure is good enough) and apply the Unity WebCamTexture to meet our purpose.
      http://answers.unity3d.com/questions/909967/getting-a-web-cam-to-play-on-ui-texture-image.html

      刪除
    2. When I use Unity WebCamTexture with high resolution in Android platform, It's performance is very slow, do you have any idea about this?
      (ps: IOS do not have performance problem)

      刪除
    3. You're right! (And it's not so surprising though, consider there might be different person to implement the Android part and the IOS part).
      In our application, it is tolerable. However, I also know it's crucial in some other cases. I've hear that the other team call OpenCV (http://opencv.org/) to open the camera instead. If you also want to edit the photo (such as cartoonize the photo, face detection... etc), OpenCV would be the best choice. In this case, you might need to know how to pass the C++ array back to Unity:
      http://answers.unity3d.com/questions/34606/how-do-i-pass-arrays-from-c-to-c-in-unity-if-at-al.html

      刪除
  2. Hi! I read your article and it is an interesting approach. Is there any way to include a character from unity (with images and motion) in front of the camera? Thank you very much! :)

    回覆刪除
    回覆
    1. Hi,
      If you want to do so, maybe the WebcamTexture that provide by Unity3D is more suitable for you. In my case, I don't need to attached the 3D character to the video. Instead, the ability to adjust the exposure of the camera is significant in my case.

      刪除