本文最后更新于:2023年4月15日 下午
- win7
- Android Studio 3.0.1
相关代码请参阅: https://github.com/RustFisher/android-CameraRecorder
概述
本文目的:使用 Android Camera API 完成音视频的采集、编码、封包成 mp4 输出
基于android.hardware.Camera
,创建一个横屏应用,实时预览摄像头图像,实现录像并输出MP4的功能。
这里不使用Camera2。
申请权限
1 2 3 4 5 6
| <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中动态申请权限
1 2 3 4 5
| 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
释放Camera
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback { private static final String TAG = "rustAppCameraPreview"; 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中显示摄像头预览
预置一个FrameLayout,实例化一个CameraPreview
添加进去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
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);
mCameraPreview = new CameraPreview(getContext()); FrameLayout preview = view.findViewById(R.id.camera_preview); preview.addView(mCameraPreview); } }
|
给MediaRecorder
指定参数后,调用start()
开始录制,stop()
结束录制
录制开始前,获取camera,mCamera.unlock()
解锁;录制完毕后,清除MediaRecorder
,mCamera.lock()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| 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
。
在Fragment中,判断销毁和重建预览的时机。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @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); }
|
参考资料