2015年7月19日 星期日

How to Access Android Camera in Unity (II): Customized android plugin by calling Android API

Overview

     In the last article, I use the Unity default WebCamTexture to read the input from camera device. However, it seems that nearly none of the camera parameters is adjustable.  In this article, I'll show you how to customized the camera by android API and how to invoke it from Unity.


Solution


How to call android plugins in Unity

      If you have no experience of building an android plugin, here is a good tutorial. Note that the "package name" in Android should match the "Bundle ID" in Unity. Also note that if you have upper case in your package name, it would be transferred to lower case automatically when you build the new project. This will result in the mismatch of package name and the Bundle ID. Although there're several websites have mentioned how to change package name, I failed. So it would be better to get it right at the beginning.

How to customize Android Camera

     You can follow the android documents step by step. There're also several examples such as here and here. However, none of them is aimed to build android plugins for Unity. Following will show you how to build customized plugin for Unity.

1. Create a script (AndroidCameraAPI.cs) to call the android plugin in Unity:
/// <summary>
/// This Class Call the Android Code to Take Picture.
/// Note: there should be a AndroidCameraAPI.jar in \Assets\Plugins\Android
/// </summary>
public class AndroidCameraAPI : MonoBehaviour
{
   private AndroidJavaObject androidCameraActivity;

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

   /// <summary>
   /// Note: Only support the "jpg" format, and you should NOT include the File Name Extension. 
   ///   For example, use
   ///   TakePhotoFromAndroidAPI(path, "myImage");
   ///   instead of
   ///   TakePhotoFromAndroidAPI(path, "myImage.jpg");
   /// </summary>
   /// <param name="_pathToSavePhoto"></param>
   /// <param name="_fileNameOfSavedPhoto"></param>
   public void TakePhotoFromAndroidAPI(string _pathToSavePhoto, string _fileNameOfSavedPhoto)
   {
      this.androidCameraActivity.Call("TakePhoto",
                                      _pathToSavePhoto,
                                      _fileNameOfSavedPhoto + ".jpg");
   }
}

2. Now edit the MainActivity.java (which can be thought as the entrance of Android) in AndroidStudio:
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 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();
    }

}
Note: You should delete the line
    setContentView(R.layout.activity_main);
in onCreate(). The reason will be illustrated in the next article.

3. The CameraDataCenter.java is used to store some camera parameters.
public class CameraDataCenter
{
    public static final int DEFAULT_CAMERA_EXPOSURE_LEVEL = 3;
    public static int userDefinedCameraExposureLevel = DEFAULT_CAMERA_EXPOSURE_LEVEL;

    public static final int DEFAULT_CAMERA_ZOOM_LEVEL = 0;
    public static int userDefinedCameraZoomLevel = DEFAULT_CAMERA_ZOOM_LEVEL;

    public static final int CAMERA_SETTING_REQUEST_CODE = 999;

    public static final int PHOTO_WIDTH = 1280;
    public static final int PHOTO_HEIGHT = 720;
}

4. The SavePictureCallBack.java is a callback for TakePhoto() in MainActivity. And will tell the system how to save the photo you just taken.
import android.hardware.Camera;
import android.util.Log;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class SavePictureCallBack implements Camera.PictureCallback
{
    private String targetPathToSavePhoto;
    private String targetFileNameOfPhoto;

    public SavePictureCallBack(final String _targetPath, final String _targetPhotoFileName)
    {
        this.targetPathToSavePhoto = _targetPath;
        this.targetFileNameOfPhoto = _targetPhotoFileName;
    }

    @Override
    public void onPictureTaken(byte[] _data, Camera _camera)
    {
        File newImageFile = new File(this.targetPathToSavePhoto, this.targetFileNameOfPhoto);
        if( newImageFile != null)
        {
            FileOutputStream fileStream = null;
            try
            {
                fileStream = new FileOutputStream(newImageFile);
                fileStream.write(_data);
                Log.i("Unity", "Save Photo to: " + newImageFile.getPath() + " Success!");
            }
            catch (Exception exception)
            {
                ExceptionManager.SaveErrorCodeToFile(ExceptionManager.CAMERA_ERROR_LOG_FILE_NAME,
                                                    "Photo is taken, but can't be saved."
                                                    + " If this error keeps happening,"
                                                    + " please Check your Disk Space");
            }
            finally
            {
                CloseFileAndReportExceptionByLog(fileStream);
            }

        }
        else
        {
            ExceptionManager.SaveErrorCodeToFile(ExceptionManager.CAMERA_ERROR_LOG_FILE_NAME,
                                                "Photo is taken, but can't be saved."
                                                + " If this error keeps happening,"
                                                + " please Check your Disk Space");

        }

    }

    public static void CloseFileAndReportExceptionByLog(FileOutputStream _fileToBeClose)
    {
        try
        {
            _fileToBeClose.close();
        }
        catch(IOException exception)
        {
            Log.w("Unity", "Close Image file Failed.");
        }
    }
}

5. Finally, the AndroidManifest.xml:
<?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>
  
  
    </application>
</manifest>


Result

     If you call the AndroidCameraAPI.TakePhoto() in Unity, it will take a snapshot immediately. The result is shown below. As you can see, after adjust the exposure, the camera  looks better now.


Conclusion

Following list the advantages and disadvantages:

Advantages:

1. You can adjust many parameters provided by android API.
2. The camera will be auto-focus when you take a picture (There'll be no auto-focus if you just take a
    screenshot from the Unity default WebCamTexture).

Disadvantages:

1. Despite the complicated procedure, you can't change the Bundle ID now. Otherwise you should
    also change the package name in AndroidStudio.
2. When you call the Android plugin, the Unity will be temporary paused. In this article, you can't
    feel it because the time for calling android plugin is too soon. In the next article, I will make a
    preview layout for camera and the system will stay in android unless the user press "Return"
    button.  In that situation, the Unity will be paused which means that the Unity can't read any IO
    and stop playing musics when the system go down to android.

5 則留言:

  1. Hi, I tested your code but failed to make it work.
    how can u do that without callling 'setPreviewDisplay' please?

    回覆刪除
    回覆
    1. Hi, do you mean that you don't want to call "androidCamera.startPreview()"? Actually, I think that there's no way you call the "androidCamera.takePicture()" without calling the "androidCamera.startPreview()".

      However, you don't need to worry about the present of the preview window (the camera video) since it will not present here unless you have some further setting (see next article). Therefore, this function will just take a quick shot of people without display the camera video. It this what you want?

      刪除
    2. Hi, i'm afraid u didn't catch me.
      On the document:
      https://developer.android.com/reference/android/hardware/Camera.html
      Step 5:
      Important: Pass a fully initialized SurfaceHolder to setPreviewDisplay(SurfaceHolder). Without a surface, the camera will be unable to start the preview.

      I didn't find the 'setPreviewDisplay(SurfaceHolder)' step in your code. According to the document, without calling setPreviewDisplay(SurfaceHolder), startPreview will fail.

      So how did you successfully call 'startPreview' without 'setPreviewDisplay' please?

      刪除
    3. Sorry for my misunderstanding. It has been a while since I wrote this article. The code post in this article surely work without the SurfaceHolder (at least one years ago). Do you get the error when building the plugin or at run time?

      Furthermore, there's another article that use SurfaceHolder and will display the camera video:
      http://ani.gamer.com.tw/animeVideo.php?sn=3431
      In this case, I surely use "setPreviewDisplay(SurfaceHolder)".

      刪除
    4. Got it! Thank you very much!

      刪除