使用 Android Camera API 完成音視頻的採集、編碼、封包成 mp4 輸出
基於android.hardware.Camera,建立一個橫屏應用,實時預覽攝像頭圖像,實現錄像並輸出MP4的功能。android
一、申請權限ide
<!-- 須要錄製音視頻權限和寫外部存儲權限 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera" />
在activity中動態申請權限this
private static final String[] VIDEO_PERMISSIONS = { Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE };
二、實現攝像頭預覽功能編碼
使用SurfaceView來預覽。新建CameraPreview類繼承自SurfaceView並實現SurfaceHolder.Callback;Camera相關操做都放在這個View裏。surfaceCreated中獲取Camera實例,啓動預覽;設置預覽相關參數,surfaceDestroyed釋放Cameracode
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback { private SurfaceHolder mHolder; private Camera mCamera; public static final int MEDIA_TYPE_IMAGE = 1; public static final int MEDIA_TYPE_VIDEO = 2; private static int mOptVideoWidth = 1920; // 默認視頻幀寬度 private static int mOptVideoHeight = 1080; private Uri outputMediaFileUri; private String outputMediaFileType; public CameraPreview(Context context) { super(context); mHolder = getHolder(); mHolder.addCallback(this); } private static Camera getCameraInstance() { Camera c = null; try { c = Camera.open(); } catch (Exception e) { Log.d(TAG, "camera is not available"); } return c; } @Override public void surfaceCreated(SurfaceHolder holder) { mCamera = getCameraInstance(); try { mCamera.setPreviewDisplay(holder); mCamera.startPreview(); getCameraOptimalVideoSize(); // 找到最合適的分辨率 } catch (IOException e) { Log.d(TAG, "Error setting camera preview: " + e.getMessage()); } } private void getCameraOptimalVideoSize() { try { Camera.Parameters parameters = mCamera.getParameters(); List<Camera.Size> mSupportedPreviewSizes = parameters.getSupportedPreviewSizes(); List<Camera.Size> mSupportedVideoSizes = parameters.getSupportedVideoSizes(); Camera.Size optimalSize = CameraHelper.getOptimalVideoSize(mSupportedVideoSizes, mSupportedPreviewSizes, getWidth(), getHeight()); mOptVideoWidth = optimalSize.width; mOptVideoHeight = optimalSize.height; Log.d(TAG, "prepareVideoRecorder: optimalSize:" + mOptVideoWidth + ", " + mOptVideoHeight); } catch (Exception e) { Log.e(TAG, "getCameraOptimalVideoSize: ", e); } } @Override public void surfaceDestroyed(SurfaceHolder holder) { mHolder.removeCallback(this); mCamera.setPreviewCallback(null); mCamera.stopPreview(); mCamera.release(); mCamera = null; } @Override public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { } }
三、在Fragment中顯示攝像頭預覽orm
預置一個FrameLayout,實例化一個CameraPreview添加進去視頻
/** * 視頻錄製界面 * Created by Rust on 2018/5/17. */ public class VideoRecordFragment extends Fragment { private static final String TAG = "rustAppVideoFrag"; private Button mCaptureBtn; private CameraPreview mCameraPreview; public static VideoRecordFragment newInstance() { return new VideoRecordFragment(); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(TAG, "frag onCreate"); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { Log.d(TAG, "frag onCreateView"); return inflater.inflate(R.layout.frag_video_record, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { Log.d(TAG, "frag onViewCreated"); super.onViewCreated(view, savedInstanceState); mCaptureBtn = view.findViewById(R.id.capture_btn); //mCaptureBtn.setOnClickListener(mOnClickListener);// 錄製鍵 mCameraPreview = new CameraPreview(getContext()); FrameLayout preview = view.findViewById(R.id.camera_preview); preview.addView(mCameraPreview); } }
使用MediaRecorder錄製
給MediaRecorder指定參數後,調用start()開始錄製,stop()結束錄製繼承
錄製開始前,獲取camera,mCamera.unlock()解鎖;錄製完畢後,清除MediaRecorder,mCamera.lock()rem
private MediaRecorder mMediaRecorder; public boolean startRecording() { if (prepareVideoRecorder()) { mMediaRecorder.start(); return true; } else { releaseMediaRecorder(); } return false; } public void stopRecording() { if (mMediaRecorder != null) { mMediaRecorder.stop(); } releaseMediaRecorder(); } public boolean isRecording() { return mMediaRecorder != null; } private boolean prepareVideoRecorder() { if (null == mCamera) { mCamera = getCameraInstance(); } mMediaRecorder = new MediaRecorder(); mCamera.unlock(); mMediaRecorder.setCamera(mCamera); mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT); mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); mMediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH)); mMediaRecorder.setVideoSize(mOptVideoWidth, mOptVideoHeight); mMediaRecorder.setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()); mMediaRecorder.setPreviewDisplay(mHolder.getSurface()); try { mMediaRecorder.prepare(); } catch (IllegalStateException e) { Log.d(TAG, "IllegalStateException preparing MediaRecorder: " + e.getMessage()); releaseMediaRecorder(); return false; } catch (IOException e) { Log.d(TAG, "IOException preparing MediaRecorder: " + e.getMessage()); releaseMediaRecorder(); return false; } return true; } private void releaseMediaRecorder() { if (mMediaRecorder != null) { mMediaRecorder.reset(); mMediaRecorder.release(); mMediaRecorder = null; mCamera.lock(); } } private File getOutputMediaFile(int type) { File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_PICTURES), TAG); if (!mediaStorageDir.exists()) { if (!mediaStorageDir.mkdirs()) { Log.d(TAG, "failed to create directory"); return null; } } String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(new Date()); File mediaFile; if (type == MEDIA_TYPE_IMAGE) { mediaFile = new File(mediaStorageDir.getPath() + File.separator + "IMG_" + timeStamp + ".jpg"); outputMediaFileType = "image/*"; } else if (type == MEDIA_TYPE_VIDEO) { mediaFile = new File(mediaStorageDir.getPath() + File.separator + "VID_" + timeStamp + ".mp4"); outputMediaFileType = "video/*"; } else { return null; } outputMediaFileUri = Uri.fromFile(mediaFile); return mediaFile; }
後臺返回時預覽黑屏的問題
CameraPreview是咱們在Fragment建立時實例化並添加進去的。
應用退到後臺後,CameraPreview已經被銷燬。應用回到前臺時,咱們應該在onResume方法中進行操做。恢復CameraPreview。get
在Fragment中,判斷銷燬和重建預覽的時機。
@Override public void onPause() { super.onPause(); Log.d(TAG, "onPause: 銷燬預覽"); mCameraPreview = null; } @Override public void onResume() { super.onResume(); Log.d(TAG, "onResume: 回到前臺"); if (null == mCameraPreview) { initCameraPreview(); } } private void initCameraPreview() { mCameraPreview = new CameraPreview(getContext()); FrameLayout preview = mRoot.findViewById(R.id.camera_preview); preview.addView(mCameraPreview); }
原創做者:CHSmile,原文連接:https://www.jianshu.com/p/b6386ba20d08