Home | History | Annotate | Download | only in camera2video
      1 /*
      2  * Copyright 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *       http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.example.android.camera2video;
     18 
     19 import android.app.Activity;
     20 import android.app.AlertDialog;
     21 import android.app.Dialog;
     22 import android.app.DialogFragment;
     23 import android.app.Fragment;
     24 import android.content.Context;
     25 import android.content.DialogInterface;
     26 import android.content.res.Configuration;
     27 import android.graphics.Matrix;
     28 import android.graphics.RectF;
     29 import android.graphics.SurfaceTexture;
     30 import android.hardware.camera2.CameraAccessException;
     31 import android.hardware.camera2.CameraCaptureSession;
     32 import android.hardware.camera2.CameraCharacteristics;
     33 import android.hardware.camera2.CameraDevice;
     34 import android.hardware.camera2.CameraManager;
     35 import android.hardware.camera2.CameraMetadata;
     36 import android.hardware.camera2.CaptureRequest;
     37 import android.hardware.camera2.params.StreamConfigurationMap;
     38 import android.media.MediaRecorder;
     39 import android.os.Bundle;
     40 import android.os.Handler;
     41 import android.os.HandlerThread;
     42 import android.util.Log;
     43 import android.util.Size;
     44 import android.util.SparseIntArray;
     45 import android.view.LayoutInflater;
     46 import android.view.Surface;
     47 import android.view.TextureView;
     48 import android.view.View;
     49 import android.view.ViewGroup;
     50 import android.widget.Button;
     51 import android.widget.Toast;
     52 
     53 import java.io.File;
     54 import java.io.IOException;
     55 import java.util.ArrayList;
     56 import java.util.Collections;
     57 import java.util.Comparator;
     58 import java.util.List;
     59 import java.util.concurrent.Semaphore;
     60 import java.util.concurrent.TimeUnit;
     61 
     62 public class Camera2VideoFragment extends Fragment implements View.OnClickListener {
     63 
     64     private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
     65 
     66     private static final String TAG = "Camera2VideoFragment";
     67 
     68     static {
     69         ORIENTATIONS.append(Surface.ROTATION_0, 90);
     70         ORIENTATIONS.append(Surface.ROTATION_90, 0);
     71         ORIENTATIONS.append(Surface.ROTATION_180, 270);
     72         ORIENTATIONS.append(Surface.ROTATION_270, 180);
     73     }
     74 
     75     /**
     76      * An {@link AutoFitTextureView} for camera preview.
     77      */
     78     private AutoFitTextureView mTextureView;
     79 
     80     /**
     81      * Button to record video
     82      */
     83     private Button mButtonVideo;
     84 
     85     /**
     86      * A refernce to the opened {@link android.hardware.camera2.CameraDevice}.
     87      */
     88     private CameraDevice mCameraDevice;
     89 
     90     /**
     91      * A reference to the current {@link android.hardware.camera2.CameraCaptureSession} for preview.
     92      */
     93     private CameraCaptureSession mPreviewSession;
     94 
     95     /**
     96      * {@link TextureView.SurfaceTextureListener} handles several lifecycle events on a
     97      * {@link TextureView}.
     98      */
     99     private TextureView.SurfaceTextureListener mSurfaceTextureListener
    100             = new TextureView.SurfaceTextureListener() {
    101 
    102         @Override
    103         public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture,
    104                                               int width, int height) {
    105             openCamera(width, height);
    106         }
    107 
    108         @Override
    109         public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture,
    110                                                 int width, int height) {
    111             configureTransform(width, height);
    112         }
    113 
    114         @Override
    115         public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
    116             return true;
    117         }
    118 
    119         @Override
    120         public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
    121         }
    122 
    123     };
    124 
    125     /**
    126      * The {@link android.util.Size} of camera preview.
    127      */
    128     private Size mPreviewSize;
    129 
    130     /**
    131      * The {@link android.util.Size} of video recording.
    132      */
    133     private Size mVideoSize;
    134 
    135     /**
    136      * Camera preview.
    137      */
    138     private CaptureRequest.Builder mPreviewBuilder;
    139 
    140     /**
    141      * MediaRecorder
    142      */
    143     private MediaRecorder mMediaRecorder;
    144 
    145     /**
    146      * Whether the app is recording video now
    147      */
    148     private boolean mIsRecordingVideo;
    149 
    150     /**
    151      * An additional thread for running tasks that shouldn't block the UI.
    152      */
    153     private HandlerThread mBackgroundThread;
    154 
    155     /**
    156      * A {@link Handler} for running tasks in the background.
    157      */
    158     private Handler mBackgroundHandler;
    159 
    160     /**
    161      * A {@link Semaphore} to prevent the app from exiting before closing the camera.
    162      */
    163     private Semaphore mCameraOpenCloseLock = new Semaphore(1);
    164 
    165     /**
    166      * {@link CameraDevice.StateCallback} is called when {@link CameraDevice} changes its status.
    167      */
    168     private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
    169 
    170         @Override
    171         public void onOpened(CameraDevice cameraDevice) {
    172             mCameraDevice = cameraDevice;
    173             startPreview();
    174             mCameraOpenCloseLock.release();
    175             if (null != mTextureView) {
    176                 configureTransform(mTextureView.getWidth(), mTextureView.getHeight());
    177             }
    178         }
    179 
    180         @Override
    181         public void onDisconnected(CameraDevice cameraDevice) {
    182             mCameraOpenCloseLock.release();
    183             cameraDevice.close();
    184             mCameraDevice = null;
    185         }
    186 
    187         @Override
    188         public void onError(CameraDevice cameraDevice, int error) {
    189             mCameraOpenCloseLock.release();
    190             cameraDevice.close();
    191             mCameraDevice = null;
    192             Activity activity = getActivity();
    193             if (null != activity) {
    194                 activity.finish();
    195             }
    196         }
    197 
    198     };
    199 
    200     public static Camera2VideoFragment newInstance() {
    201         Camera2VideoFragment fragment = new Camera2VideoFragment();
    202         fragment.setRetainInstance(true);
    203         return fragment;
    204     }
    205 
    206     /**
    207      * In this sample, we choose a video size with 3x4 aspect ratio. Also, we don't use sizes larger
    208      * than 1080p, since MediaRecorder cannot handle such a high-resolution video.
    209      *
    210      * @param choices The list of available sizes
    211      * @return The video size
    212      */
    213     private static Size chooseVideoSize(Size[] choices) {
    214         for (Size size : choices) {
    215             if (size.getWidth() == size.getHeight() * 4 / 3 && size.getWidth() <= 1080) {
    216                 return size;
    217             }
    218         }
    219         Log.e(TAG, "Couldn't find any suitable video size");
    220         return choices[choices.length - 1];
    221     }
    222 
    223     /**
    224      * Given {@code choices} of {@code Size}s supported by a camera, chooses the smallest one whose
    225      * width and height are at least as large as the respective requested values, and whose aspect
    226      * ratio matches with the specified value.
    227      *
    228      * @param choices     The list of sizes that the camera supports for the intended output class
    229      * @param width       The minimum desired width
    230      * @param height      The minimum desired height
    231      * @param aspectRatio The aspect ratio
    232      * @return The optimal {@code Size}, or an arbitrary one if none were big enough
    233      */
    234     private static Size chooseOptimalSize(Size[] choices, int width, int height, Size aspectRatio) {
    235         // Collect the supported resolutions that are at least as big as the preview Surface
    236         List<Size> bigEnough = new ArrayList<Size>();
    237         int w = aspectRatio.getWidth();
    238         int h = aspectRatio.getHeight();
    239         for (Size option : choices) {
    240             if (option.getHeight() == option.getWidth() * h / w &&
    241                     option.getWidth() >= width && option.getHeight() >= height) {
    242                 bigEnough.add(option);
    243             }
    244         }
    245 
    246         // Pick the smallest of those, assuming we found any
    247         if (bigEnough.size() > 0) {
    248             return Collections.min(bigEnough, new CompareSizesByArea());
    249         } else {
    250             Log.e(TAG, "Couldn't find any suitable preview size");
    251             return choices[0];
    252         }
    253     }
    254 
    255     @Override
    256     public View onCreateView(LayoutInflater inflater, ViewGroup container,
    257                              Bundle savedInstanceState) {
    258         return inflater.inflate(R.layout.fragment_camera2_video, container, false);
    259     }
    260 
    261     @Override
    262     public void onViewCreated(final View view, Bundle savedInstanceState) {
    263         mTextureView = (AutoFitTextureView) view.findViewById(R.id.texture);
    264         mButtonVideo = (Button) view.findViewById(R.id.video);
    265         mButtonVideo.setOnClickListener(this);
    266         view.findViewById(R.id.info).setOnClickListener(this);
    267     }
    268 
    269     @Override
    270     public void onResume() {
    271         super.onResume();
    272         startBackgroundThread();
    273         if (mTextureView.isAvailable()) {
    274             openCamera(mTextureView.getWidth(), mTextureView.getHeight());
    275         } else {
    276             mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
    277         }
    278     }
    279 
    280     @Override
    281     public void onPause() {
    282         closeCamera();
    283         stopBackgroundThread();
    284         super.onPause();
    285     }
    286 
    287     @Override
    288     public void onClick(View view) {
    289         switch (view.getId()) {
    290             case R.id.video: {
    291                 if (mIsRecordingVideo) {
    292                     stopRecordingVideo();
    293                 } else {
    294                     startRecordingVideo();
    295                 }
    296                 break;
    297             }
    298             case R.id.info: {
    299                 Activity activity = getActivity();
    300                 if (null != activity) {
    301                     new AlertDialog.Builder(activity)
    302                             .setMessage(R.string.intro_message)
    303                             .setPositiveButton(android.R.string.ok, null)
    304                             .show();
    305                 }
    306                 break;
    307             }
    308         }
    309     }
    310 
    311     /**
    312      * Starts a background thread and its {@link Handler}.
    313      */
    314     private void startBackgroundThread() {
    315         mBackgroundThread = new HandlerThread("CameraBackground");
    316         mBackgroundThread.start();
    317         mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
    318     }
    319 
    320     /**
    321      * Stops the background thread and its {@link Handler}.
    322      */
    323     private void stopBackgroundThread() {
    324         mBackgroundThread.quitSafely();
    325         try {
    326             mBackgroundThread.join();
    327             mBackgroundThread = null;
    328             mBackgroundHandler = null;
    329         } catch (InterruptedException e) {
    330             e.printStackTrace();
    331         }
    332     }
    333 
    334     /**
    335      * Tries to open a {@link CameraDevice}. The result is listened by `mStateCallback`.
    336      */
    337     private void openCamera(int width, int height) {
    338         final Activity activity = getActivity();
    339         if (null == activity || activity.isFinishing()) {
    340             return;
    341         }
    342         CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
    343         try {
    344             if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
    345                 throw new RuntimeException("Time out waiting to lock camera opening.");
    346             }
    347             String cameraId = manager.getCameraIdList()[0];
    348 
    349             // Choose the sizes for camera preview and video recording
    350             CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
    351             StreamConfigurationMap map = characteristics
    352                     .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
    353             mVideoSize = chooseVideoSize(map.getOutputSizes(MediaRecorder.class));
    354             mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
    355                     width, height, mVideoSize);
    356 
    357             int orientation = getResources().getConfiguration().orientation;
    358             if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
    359                 mTextureView.setAspectRatio(mPreviewSize.getWidth(), mPreviewSize.getHeight());
    360             } else {
    361                 mTextureView.setAspectRatio(mPreviewSize.getHeight(), mPreviewSize.getWidth());
    362             }
    363             configureTransform(width, height);
    364             mMediaRecorder = new MediaRecorder();
    365             manager.openCamera(cameraId, mStateCallback, null);
    366         } catch (CameraAccessException e) {
    367             Toast.makeText(activity, "Cannot access the camera.", Toast.LENGTH_SHORT).show();
    368             activity.finish();
    369         } catch (NullPointerException e) {
    370             // Currently an NPE is thrown when the Camera2API is used but not supported on the
    371             // device this code runs.
    372             new ErrorDialog().show(getFragmentManager(), "dialog");
    373         } catch (InterruptedException e) {
    374             throw new RuntimeException("Interrupted while trying to lock camera opening.");
    375         }
    376     }
    377 
    378     private void closeCamera() {
    379         try {
    380             mCameraOpenCloseLock.acquire();
    381             if (null != mCameraDevice) {
    382                 mCameraDevice.close();
    383                 mCameraDevice = null;
    384             }
    385             if (null != mMediaRecorder) {
    386                 mMediaRecorder.release();
    387                 mMediaRecorder = null;
    388             }
    389         } catch (InterruptedException e) {
    390             throw new RuntimeException("Interrupted while trying to lock camera closing.");
    391         } finally {
    392              mCameraOpenCloseLock.release();
    393         }
    394     }
    395 
    396     /**
    397      * Start the camera preview.
    398      */
    399     private void startPreview() {
    400         if (null == mCameraDevice || !mTextureView.isAvailable() || null == mPreviewSize) {
    401             return;
    402         }
    403         try {
    404             setUpMediaRecorder();
    405             SurfaceTexture texture = mTextureView.getSurfaceTexture();
    406             assert texture != null;
    407             texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
    408             mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
    409             List<Surface> surfaces = new ArrayList<Surface>();
    410 
    411             Surface previewSurface = new Surface(texture);
    412             surfaces.add(previewSurface);
    413             mPreviewBuilder.addTarget(previewSurface);
    414 
    415             Surface recorderSurface = mMediaRecorder.getSurface();
    416             surfaces.add(recorderSurface);
    417             mPreviewBuilder.addTarget(recorderSurface);
    418 
    419             mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
    420 
    421                 @Override
    422                 public void onConfigured(CameraCaptureSession cameraCaptureSession) {
    423                     mPreviewSession = cameraCaptureSession;
    424                     updatePreview();
    425                 }
    426 
    427                 @Override
    428                 public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
    429                     Activity activity = getActivity();
    430                     if (null != activity) {
    431                         Toast.makeText(activity, "Failed", Toast.LENGTH_SHORT).show();
    432                     }
    433                 }
    434             }, mBackgroundHandler);
    435         } catch (CameraAccessException e) {
    436             e.printStackTrace();
    437         } catch (IOException e) {
    438             e.printStackTrace();
    439         }
    440     }
    441 
    442     /**
    443      * Update the camera preview. {@link #startPreview()} needs to be called in advance.
    444      */
    445     private void updatePreview() {
    446         if (null == mCameraDevice) {
    447             return;
    448         }
    449         try {
    450             setUpCaptureRequestBuilder(mPreviewBuilder);
    451             HandlerThread thread = new HandlerThread("CameraPreview");
    452             thread.start();
    453             mPreviewSession.setRepeatingRequest(mPreviewBuilder.build(), null, mBackgroundHandler);
    454         } catch (CameraAccessException e) {
    455             e.printStackTrace();
    456         }
    457     }
    458 
    459     private void setUpCaptureRequestBuilder(CaptureRequest.Builder builder) {
    460         builder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
    461     }
    462 
    463     /**
    464      * Configures the necessary {@link android.graphics.Matrix} transformation to `mTextureView`.
    465      * This method should not to be called until the camera preview size is determined in
    466      * openCamera, or until the size of `mTextureView` is fixed.
    467      *
    468      * @param viewWidth  The width of `mTextureView`
    469      * @param viewHeight The height of `mTextureView`
    470      */
    471     private void configureTransform(int viewWidth, int viewHeight) {
    472         Activity activity = getActivity();
    473         if (null == mTextureView || null == mPreviewSize || null == activity) {
    474             return;
    475         }
    476         int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
    477         Matrix matrix = new Matrix();
    478         RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
    479         RectF bufferRect = new RectF(0, 0, mPreviewSize.getHeight(), mPreviewSize.getWidth());
    480         float centerX = viewRect.centerX();
    481         float centerY = viewRect.centerY();
    482         if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) {
    483             bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY());
    484             matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL);
    485             float scale = Math.max(
    486                     (float) viewHeight / mPreviewSize.getHeight(),
    487                     (float) viewWidth / mPreviewSize.getWidth());
    488             matrix.postScale(scale, scale, centerX, centerY);
    489             matrix.postRotate(90 * (rotation - 2), centerX, centerY);
    490         }
    491         mTextureView.setTransform(matrix);
    492     }
    493 
    494     private void setUpMediaRecorder() throws IOException {
    495         final Activity activity = getActivity();
    496         if (null == activity) {
    497             return;
    498         }
    499         mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
    500         mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
    501         mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
    502         mMediaRecorder.setOutputFile(getVideoFile(activity).getAbsolutePath());
    503         mMediaRecorder.setVideoEncodingBitRate(10000000);
    504         mMediaRecorder.setVideoFrameRate(30);
    505         mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight());
    506         mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
    507         mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
    508         int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
    509         int orientation = ORIENTATIONS.get(rotation);
    510         mMediaRecorder.setOrientationHint(orientation);
    511         mMediaRecorder.prepare();
    512     }
    513 
    514     private File getVideoFile(Context context) {
    515         return new File(context.getExternalFilesDir(null), "video.mp4");
    516     }
    517 
    518     private void startRecordingVideo() {
    519         try {
    520             // UI
    521             mButtonVideo.setText(R.string.stop);
    522             mIsRecordingVideo = true;
    523 
    524             // Start recording
    525             mMediaRecorder.start();
    526         } catch (IllegalStateException e) {
    527             e.printStackTrace();
    528         }
    529     }
    530 
    531     private void stopRecordingVideo() {
    532         // UI
    533         mIsRecordingVideo = false;
    534         mButtonVideo.setText(R.string.record);
    535         // Stop recording
    536         mMediaRecorder.stop();
    537         mMediaRecorder.reset();
    538         Activity activity = getActivity();
    539         if (null != activity) {
    540             Toast.makeText(activity, "Video saved: " + getVideoFile(activity),
    541                     Toast.LENGTH_SHORT).show();
    542         }
    543         startPreview();
    544     }
    545 
    546     /**
    547      * Compares two {@code Size}s based on their areas.
    548      */
    549     static class CompareSizesByArea implements Comparator<Size> {
    550 
    551         @Override
    552         public int compare(Size lhs, Size rhs) {
    553             // We cast here to ensure the multiplications won't overflow
    554             return Long.signum((long) lhs.getWidth() * lhs.getHeight() -
    555                     (long) rhs.getWidth() * rhs.getHeight());
    556         }
    557 
    558     }
    559 
    560     public static class ErrorDialog extends DialogFragment {
    561 
    562         @Override
    563         public Dialog onCreateDialog(Bundle savedInstanceState) {
    564             final Activity activity = getActivity();
    565             return new AlertDialog.Builder(activity)
    566                     .setMessage("This device doesn't support Camera2 API.")
    567                     .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
    568                         @Override
    569                         public void onClick(DialogInterface dialogInterface, int i) {
    570                             activity.finish();
    571                         }
    572                     })
    573                     .create();
    574         }
    575 
    576     }
    577 
    578 }
    579