Android项目小结——可对焦的视频录制(MediaRecorder与TextureView实现)
一直在做安卓的项目,想着找个时间总结一下,可能太懒了,一直没总结。
代码(尤其是对焦框显示)参考了许多Blog和Github,修修补补改改挺多地方,记录一下,侵删私信或注明出处。
录制
主要的类
- Camera2Helper:基于Camera2的Helper类,定义一些参数、回调、状态(Resume、Pause、Destroy等)等信息。
- AutoFitTextureView:继承自TextureView,实现自定义纵横比缩放,视频录制基于该组件。
- AnimationImageView:对焦框的View动画组件【可选】。
- PreviewSessionCallback:预览回调和对焦监听。
- TextureViewTouchEvent:对焦实现。
录制流程
(1)初始化Camera2Helper
(2)调用Camera2的API,读取摄像头参数,拿到支持的分辨率(分为预览(Preview)
分辨率和录制(MediaCoder)
分辨率,下面采用的是一致的,也可以不一致的)
(3)打开摄像头,开始预览(输出到预览Surface和录制Surface)
(4)开始录制(封装成startRecord方法,内部调用MediaCoder.start()
方法)
(5)结束录制
涉及的点
- 平板上录制视频,默认方向是平板横评录制,故采用
TextureView
进行显示,可以用View的旋转方法进行显示。 - MediaCoder录制速度非常快(硬编码,基于MediaCodec吧),竖屏录制,设置旋转参数即可。
- 分辨率的选择,MediaCoder可以完成进行4K视频(3840x2160)的录制,但还要看具体机型的硬编码支持的分辨率(adb查看/system/etc/xxx_codec.xml)和摄像头支持的分辨率(camera2API查看)(如,SM-T820摄像头可预览4128x3096的视频,却不能设置给MediaCoder,没阅读源码,有知道的朋友可以评论告诉我,Thanks♪(・ω・)ノ)
- 工具类和Activity状态绑定。
- 【可选】把握对焦的几个状态,关键词Camera2 3A 状态转换(该博客有翻译)
- 【可选】对焦区域的测量,也就是触摸View后,矩形框的定位参考
代码实现
组件一:AutoFitTextureView
录制和预览界面。
public class AutoFitTextureView extends TextureView {private int mRatioWidth = 0;private int mRatioHeight = 0;private MyTextureViewTouchEvent mMyTextureViewTouchEvent;private FocusPositionTouchEvent mFocusPositionTouchEvent;public AutoFitTextureView(Context context) {this(context, null);}public AutoFitTextureView(Context context, AttributeSet attrs) {this(context, attrs, 0);}public AutoFitTextureView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}/*** Sets the aspect ratio for this view. The size of the view will be measured based on the ratio* calculated from the parameters. Note that the actual sizes of parameters don't matter, that* is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result.** @param width Relative horizontal size* @param height Relative vertical size*/public void setAspectRatio(int width, int height) {if (width < 0 || height < 0) {throw new IllegalArgumentException("Size cannot be negative.");}mRatioWidth = width;mRatioHeight = height;requestLayout();}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int width = MeasureSpec.getSize(widthMeasureSpec);int height = MeasureSpec.getSize(heightMeasureSpec);if (0 == mRatioWidth || 0 == mRatioHeight) {setMeasuredDimension(width, height);} else {if (width < height * mRatioWidth / mRatioHeight) {setMeasuredDimension(width, width * mRatioHeight / mRatioWidth);} else {setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);}}}@Overridepublic boolean onTouchEvent(MotionEvent event) {mFocusPositionTouchEvent.getPosition(event);if (mMyTextureViewTouchEvent != null) {return mMyTextureViewTouchEvent.onAreaTouchEvent(event);}return true;}public void setMyTextureViewTouchEvent(MyTextureViewTouchEvent myTextureViewTouchEvent) {this.mMyTextureViewTouchEvent = myTextureViewTouchEvent;}public void setFocusPositionTouchEvent(FocusPositionTouchEvent mFocusPositionTouchEvent) {this.mFocusPositionTouchEvent = mFocusPositionTouchEvent;}public interface MyTextureViewTouchEvent {boolean onAreaTouchEvent(MotionEvent event);}public interface FocusPositionTouchEvent {void getPosition(MotionEvent event);}}
注:
(1)定义两个接口,getPosition
用于界面层的对焦框位置获取;onAreaTouchEvent
用于预览层的对焦点定位,设置参数,重新发送预览请求。
(2)一个公开的方法,设置纵横比,保证预览不变形。
(3)覆写onMeasure,通过(2)的方法,保证预览不变形。
(4)覆写Touch事件,调用两个监听器方法(1中的接口)。
组件二:AnimationImageView
public class AnimationImageView extends android.support.v7.widget.AppCompatImageView {private Handler mMainHandler;private Animation mAnimation;private Context mContext;/*** 防止又换了个框,但是上次哪个还没有消失即将消失,就把新的框的给消失了*/public int mTimes = 0;public AnimationImageView(Context context) {super(context);mContext = context;}public AnimationImageView(Context context, AttributeSet attrs) {super(context, attrs);mContext = context;}public AnimationImageView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);mContext = context;}public void setmMainHandler(Handler mMainHandler) {this.mMainHandler = mMainHandler;}public void setmAnimation(Animation mAnimation) {this.mAnimation = mAnimation;}public void initFocus() {this.setVisibility(VISIBLE);new Thread(new SleepThread(mMainHandler, Camera2Helper.FOCUS_DISAPPEAR, 1, null)).start();}public void startFocusing() {mTimes++;this.setVisibility(View.VISIBLE);this.startAnimation(mAnimation);this.setBackground(mContext.getDrawable(R.mipmap.ic_focus_start));new Thread(new SleepThread(mMainHandler, Camera2Helper.FOCUS_DISAPPEAR, 1000, mTimes)).start();}public void focusFailed() {mTimes++;this.setBackground(mContext.getDrawable(R.mipmap.ic_focus_failed));new Thread(new SleepThread(mMainHandler, Camera2Helper.FOCUS_DISAPPEAR, 800, mTimes)).start();}public void focusSuccess() {mTimes++;this.setVisibility(View.VISIBLE);this.setBackground(mContext.getDrawable(R.mipmap.ic_focus_succeed));new Thread(new SleepThread(mMainHandler, Camera2Helper.FOCUS_DISAPPEAR, 800, mTimes)).start();}public void stopFocus() {this.setVisibility(INVISIBLE);}
}
休眠线程,发送time,判断是否是同一次的对焦状态传递。
public class SleepThread implements Runnable {private Handler mMainHandler;private int what;private long mTime;private Object mObject;public SleepThread(Handler mainHandler, int what, long mTime, Object mObject) {this.mMainHandler = mainHandler;this.what = what;this.mTime = mTime;this.mObject = mObject;}@Overridepublic void run() {try {Thread.sleep(mTime);} catch (InterruptedException e) {e.printStackTrace();}Message message = mMainHandler.obtainMessage();message.what = what;message.obj = mObject;mMainHandler.sendMessage(message);}
}
监听一:PreviewSessionCallback
public class PreviewSessionCallback extends CameraCaptureSession.CaptureCallback implements AutoFitTextureView.FocusPositionTouchEvent {private final static String TAG = "PreviewSessionCallback";private int mAfState = CameraMetadata.CONTROL_AF_STATE_INACTIVE;private int mRawX;private int mRawY;private boolean mFlagShowFocusImage = false;private AnimationImageView mFocusImage;/*** UI线程的Handler,更新UI用*/private Handler mMainHandler;public PreviewSessionCallback(AnimationImageView mFocusImage, Handler mMainHandler, AutoFitTextureView mAutoFitTextureView) {this.mFocusImage = mFocusImage;this.mMainHandler = mMainHandler;mAutoFitTextureView.setFocusPositionTouchEvent(this);}@Overridepublic void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull final TotalCaptureResult result) {Integer nowAfState = result.get(CaptureResult.CONTROL_AF_STATE);
// LogUtil.getInstance().d("status", "nowAfState:" + nowAfState + "mAfState:" + mAfState);//获取失败if (nowAfState == null) {return;}//这次的值与之前的一样,忽略掉if (nowAfState == mAfState) {return;}mAfState = nowAfState;mMainHandler.post(new Runnable() {@Overridepublic void run() {judgeFocus();}});}private void judgeFocus() {switch (mAfState) {case CameraMetadata.CONTROL_AF_STATE_ACTIVE_SCAN:LogUtil.getInstance().d("status", "CONTROL_AF_STATE_ACTIVE_SCAN");case CameraMetadata.CONTROL_AF_STATE_PASSIVE_SCAN:LogUtil.getInstance().d("status", "CONTROL_AF_STATE_PASSIVE_SCAN");focusFocusing();break;case CameraMetadata.CONTROL_AF_STATE_FOCUSED_LOCKED:LogUtil.getInstance().d("status", "CONTROL_AF_STATE_FOCUSED_LOCKED");case CameraMetadata.CONTROL_AF_STATE_PASSIVE_FOCUSED:LogUtil.getInstance().d("status", "CONTROL_AF_STATE_PASSIVE_FOCUSED");focusSucceed();break;case CameraMetadata.CONTROL_AF_STATE_INACTIVE:LogUtil.getInstance().d("status", "CONTROL_AF_STATE_INACTIVE");focusInactive();break;case CameraMetadata.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED:LogUtil.getInstance().d("status", "CONTROL_AF_STATE_NOT_FOCUSED_LOCKED");case CameraMetadata.CONTROL_AF_STATE_PASSIVE_UNFOCUSED:LogUtil.getInstance().d("status", "CONTROL_AF_STATE_PASSIVE_UNFOCUSED");focusFailed();break;}}private void focusFocusing() {//得到宽高int width = mFocusImage.getWidth();int height = mFocusImage.getHeight();//居中ViewGroup.MarginLayoutParams margin = new ViewGroup.MarginLayoutParams(mFocusImage.getLayoutParams());margin.setMargins(mRawX - width / 2, mRawY - height / 2, margin.rightMargin, margin.bottomMargin);RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(margin);mFocusImage.setLayoutParams(layoutParams);//显示if (!mFlagShowFocusImage) {mFocusImage.startFocusing();mFlagShowFocusImage = true;}}private void focusSucceed() {if (mFlagShowFocusImage) {mFocusImage.focusSuccess();mFlagShowFocusImage = false;}}private void focusInactive() {mFocusImage.stopFocus();mFlagShowFocusImage = false;}private void focusFailed() {if (mFlagShowFocusImage) {mFocusImage.focusFailed();mFlagShowFocusImage = false;}}@Overridepublic void getPosition(MotionEvent event) {mRawX = (int) event.getRawX();mRawY = (int) event.getRawY();}
}
注:
(1)onCaptureCompleted方法当图像捕获完全完成且所有结果元数据都可用时都会调用此方法。由于请求时Repeating的,所以会不断的调用该方法,从而连续对焦,锁定等。
监听二:TextureViewTouchEvent
public class TextureViewTouchEvent implements AutoFitTextureView.MyTextureViewTouchEvent {private CameraCharacteristics mCameraCharacteristics;private TextureView mTextureView;private CaptureRequest.Builder mPreviewBuilder;private CameraCaptureSession mPreviewSession;private CaptureRequest mPreviewRequest;private Handler mBackgroundHandler;private PreviewSessionCallback mPreviewSessionCallback;private Size mPreviewSize;private Integer mSensorOrientation;public TextureViewTouchEvent(CameraCharacteristics mCameraCharacteristics, TextureView mTextureView, CaptureRequest.Builder mPreviewBuilder,CameraCaptureSession mPreviewSession, CaptureRequest mPreviewRequest, Handler mBackgroundHandler,PreviewSessionCallback mPreviewSessionCallback, Size mPreviewSize, Integer mSensorOrientation) {this.mCameraCharacteristics = mCameraCharacteristics;this.mTextureView = mTextureView;this.mPreviewBuilder = mPreviewBuilder;this.mPreviewSession = mPreviewSession;this.mPreviewRequest = mPreviewRequest;this.mBackgroundHandler = mBackgroundHandler;this.mPreviewSessionCallback = mPreviewSessionCallback;this.mPreviewSize = mPreviewSize;this.mSensorOrientation = mSensorOrientation;}@Overridepublic boolean onAreaTouchEvent(MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN:// 先取相对于view上面的坐标double x = event.getX(), y = event.getY(), tmp;LogUtil.getInstance().d("shb", "原始: x--->" + x + ",,,y--->" + y);// 取出来的图像如果有旋转角度的话,则需要将宽高交换下int realPreviewWidth;int realPreviewHeight;if (Camera2Helper.SENSOR_ORIENTATION_DEFAULT_DEGREES == mSensorOrientation || Camera2Helper.SENSOR_ORIENTATION_INVERSE_DEGREES == mSensorOrientation) {realPreviewWidth = mPreviewSize.getHeight();realPreviewHeight = mPreviewSize.getWidth();} else {realPreviewWidth = mPreviewSize.getWidth();realPreviewHeight = mPreviewSize.getHeight();}// 计算摄像头取出的图像相对于view放大了多少,以及有多少偏移double imgScale = 1.0, verticalOffset = 0, horizontalOffset = 0;// mTextureView预览View的控件if (realPreviewHeight * mTextureView.getWidth() > realPreviewWidth * mTextureView.getHeight()) {imgScale = mTextureView.getWidth() * 1.0 / realPreviewWidth;verticalOffset = (realPreviewHeight - mTextureView.getHeight() / imgScale) / 2;} else {imgScale = mTextureView.getHeight() * 1.0 / realPreviewHeight;horizontalOffset = (realPreviewWidth - mTextureView.getWidth() / imgScale) / 2;}// 将点击的坐标转换为图像上的坐标x = x / imgScale + horizontalOffset;y = y / imgScale + verticalOffset;if (Camera2Helper.SENSOR_ORIENTATION_DEFAULT_DEGREES == mSensorOrientation) {tmp = x;x = y;y = mPreviewSize.getHeight() - tmp;} else if (Camera2Helper.SENSOR_ORIENTATION_INVERSE_DEGREES == mSensorOrientation) {tmp = x;x = mPreviewSize.getWidth() - y;y = tmp;}// 计算取到的图像相对于裁剪区域的缩放系数,以及位移Rect cropRegion = mPreviewRequest.get(CaptureRequest.SCALER_CROP_REGION);if (null == cropRegion) {LogUtil.getInstance().e("TextureViewTouchEvent", "can't get crop region");Size s = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE);if (s != null) {cropRegion = new Rect(0, 0, s.getWidth(), s.getHeight());}}int cropWidth = cropRegion.width(), cropHeight = cropRegion.height();if (mPreviewSize.getHeight() * cropWidth > mPreviewSize.getWidth() * cropHeight) {imgScale = cropHeight * 1.0 / mPreviewSize.getHeight();verticalOffset = 0;horizontalOffset = (cropWidth - imgScale * mPreviewSize.getWidth()) / 2;} else {imgScale = cropWidth * 1.0 / mPreviewSize.getWidth();horizontalOffset = 0;verticalOffset = (cropHeight - imgScale * mPreviewSize.getHeight()) / 2;}// 将点击区域相对于图像的坐标,转化为相对于成像区域的坐标x = x * imgScale + horizontalOffset + cropRegion.left;y = y * imgScale + verticalOffset + cropRegion.top;double tapAreaRatio = 0.03;Rect rect = new Rect();rect.left = MathUtils.clamp((int) (x - tapAreaRatio / 2 * cropRegion.width()), 0, cropRegion.width() - 1);rect.right = MathUtils.clamp((int) (x + tapAreaRatio / 2 * cropRegion.width()), 0, cropRegion.width() - 1);rect.top = MathUtils.clamp((int) (y - tapAreaRatio / 2 * cropRegion.width()), 0, cropRegion.height() - 1);rect.bottom = MathUtils.clamp((int) (y + tapAreaRatio / 2 * cropRegion.width()), 0, cropRegion.height() - 1);mPreviewBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);mPreviewBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL);mPreviewBuilder.set(CaptureRequest.CONTROL_AE_LOCK, Boolean.FALSE);mPreviewBuilder.set(CaptureRequest.CONTROL_AF_REGIONS, new MeteringRectangle[]{new MeteringRectangle(rect, 999)});mPreviewBuilder.set(CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[]{new MeteringRectangle(rect, 999)});mPreviewBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);mPreviewBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START);try {mPreviewSession.setRepeatingRequest(mPreviewBuilder.build(), mPreviewSessionCallback, mBackgroundHandler);} catch (CameraAccessException e) {LogUtil.getInstance().e("TextureViewTouchEvent", "setRepeatingRequest failed, " + e.getMessage());}break;case MotionEvent.ACTION_UP:break;}return true;}}
Camera2Helper
相机锁
private Semaphore mCameraOpenCloseLock = new Semaphore(1);
mMainHandler(UI线程的Looper,可更新UI)
/*** UI线程的handler*/private Handler mMainHandler = new Handler(Looper.getMainLooper()) {@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);switch (msg.what) {case FOCUS_DISAPPEAR:if (msg.obj == null) {mFocusImage.stopFocus();break;}Integer valueTimes = (Integer) msg.obj;if (mFocusImage.mTimes == valueTimes) {mFocusImage.stopFocus();}break;}}};
后台线程(用这个线程去进行预览,不会阻塞UI线程)
/*** <p>开启一个后台线程,防止阻塞主UI</p>* An additional thread for running tasks that shouldn't block the UI.*/private HandlerThread mBackgroundThread;/*** <p>后台线程的Handler</p>* A {@link Handler} for running tasks in the background.*/private Handler mBackgroundHandler;/*** 开启一个后台线程,不会阻塞UI<p>* Starts a background thread and its {@link Handler}.*/private void startBackgroundThread() {mBackgroundThread = new HandlerThread("CameraBackground");mBackgroundThread.start();mBackgroundHandler = new Handler(mBackgroundThread.getLooper());}/*** 停止后台线程<p>* Stops the background thread and its {@link Handler}.*/private void stopBackgroundThread() {if (mBackgroundThread != null) {mBackgroundThread.quitSafely();try {mBackgroundThread.join();mBackgroundThread = null;mBackgroundHandler = null;} catch (InterruptedException e) {e.printStackTrace();}}}
构造函数
/*** @param activity 当前调用的Activity* @param mAutoFitTextureView 录制的预览View* @param videoAbsolutePath 视频保存路径*/public Camera2Helper(Activity activity, AutoFitTextureView mAutoFitTextureView, String videoAbsolutePath) {this.mActivity = activity;this.mAutoFitTextureView = mAutoFitTextureView;this.videoAbsolutePath = videoAbsolutePath;// 线程池mExecutorService = Executors.newFixedThreadPool(3);// [可选]初始化动画[对焦]mScaleFocusAnimation = new ScaleAnimation(2.0f, 1.0f, 2.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);mScaleFocusAnimation.setDuration(200);// 初始化对焦组件 step1mFocusImage = activity.findViewById(R.id.videoRecord_focus);mFocusImage.setVisibility(View.INVISIBLE);mFocusImage.setmMainHandler(mMainHandler);mFocusImage.setmAnimation(mScaleFocusAnimation);// 初始化控件 step2RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mActivity.getResources().getDisplayMetrics().widthPixels * 0.03f, activity.getResources().getDisplayMetrics()),(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mActivity.getResources().getDisplayMetrics().widthPixels * 0.03f, activity.getResources().getDisplayMetrics()));layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);mFocusImage.setLayoutParams(layoutParams);mFocusImage.initFocus();// 初始化摄像头参数等initParameter();}
初始化参数
/*** 初始化参数,相机锁加锁*/private void initParameter() {if (!hasPermissionsGranted(VIDEO_PERMISSIONS)) {LogUtil.getInstance().d(TAG, "无权限");return;}final Activity activity = mActivity;if (null == activity || activity.isFinishing()) {return;}mCameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);try {LogUtil.getInstance().d(TAG, "tryAcquire");// 拿到相机锁if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {throw new RuntimeException("Time out waiting to lock camera opening.");}// 后置摄像头是0,多个的话,可测试对应的摄像头序号cameraId = mCameraManager.getCameraIdList()[0];// Choose the sizes for camera preview and video recordingmCameraCharacteristics = mCameraManager.getCameraCharacteristics(cameraId);StreamConfigurationMap map = mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);// 顺时针角度,输出图像需要通过从原始方向旋转该角度到设备屏幕上。如0、90、180、270mSensorOrientation = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);if (map == null) {throw new RuntimeException("Cannot get available preview/video sizes");}// 从配置文件加载分辨率设置mVideoSize = new Size(Integer.valueOf(Config.getInstance().getProperties().getProperty("VideoWidth")), Integer.valueOf(Config.getInstance().getProperties().getProperty("VideoHeight")));// 预览分辨率和录制分辨率保持一致mPreviewSize = mVideoSize;// 自动选择适合的视频分辨率,对于视频录制,并不是所有都可用,故采用定制
// mVideoSize = chooseVideoSize(map.getOutputSizes(SurfaceTexture.class));
// mPreviewSize = chooseOptimalSize(map.getOutputSizes(MediaRecorder.class),screenWidth,screenHeight,mVideoSize);mOrientation = mActivity.getResources().getConfiguration().orientation;// 给AutoFitTextureView设置一个纵横比,使其预览图像不变形if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) {mAutoFitTextureView.setAspectRatio(mPreviewSize.getWidth(), mPreviewSize.getHeight());} else {mAutoFitTextureView.setAspectRatio(mPreviewSize.getHeight(), mPreviewSize.getWidth());}mPreviewSessionCallback = new PreviewSessionCallback(mFocusImage, mMainHandler, mAutoFitTextureView);} catch (CameraAccessException e) {LogUtil.getInstance().e(activity, "Cannot access the camera.");activity.finish();} catch (NullPointerException e) {new ErrorDialog(mActivity, mActivity.getString(R.string.camera_error)).onCreateDialog().show();} catch (InterruptedException e) {throw new RuntimeException("Interrupted while trying to lock camera opening.");}}
注:
(1)mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
,返回了此摄像机设备支持的可用流配置; 还包括每个格式/大小组合的最小帧持续时间和停顿持续时间。
(2)AutoFitTextureView设置了一个宽度高度,主要用到的是纵横比,和实际的宽高没关系(TextureView代码下面给出)。
(3)PreviewSessionCallback是预览Session的回调和对焦状态的事件捕捉。
CameraDevice.StateCallback
相机设备的状态回调。
private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {@Overridepublic void onOpened(@NonNull CameraDevice cameraDevice) {mCameraDevice = cameraDevice;if (mMediaRecorder == null) {mMediaRecorder = new MediaRecorder();}// 开始预览的方法,同时输出到预览Surface和MediaCoderreadyToPreview();// 释放相机锁mCameraOpenCloseLock.release();if (null != mAutoFitTextureView) {configureTransform(mAutoFitTextureView.getWidth(), mAutoFitTextureView.getHeight());}}@Overridepublic void onDisconnected(@NonNull CameraDevice cameraDevice) {mCameraOpenCloseLock.release();cameraDevice.close();mCameraDevice = null;}@Overridepublic void onError(@NonNull CameraDevice cameraDevice, int error) {mCameraOpenCloseLock.release();cameraDevice.close();mCameraDevice = null;Activity activity = mActivity;if (null != activity) {activity.finish();}}};
注:
(1)onOpened回调时,摄像头已经打开准备就绪,可以创建CaptureSession去进行捕捉画面等操作。
(2)关闭或错误时,要释放相机锁,保证相机被释放。
涉及的方法
/*** 设置预览请求和MediaRecorder的参数*/private void readyToPreview() {if (null == mCameraDevice || !mAutoFitTextureView.isAvailable() || null == mPreviewSize) {return;}try {// 关闭先前的预览SessionclosePreviewSession();// 配置MediaCoder,并初始化setupMediaRecorder();SurfaceTexture surfaceTexture = mAutoFitTextureView.getSurfaceTexture();assert surfaceTexture != null;// 设置预览大小,全靠此方法确定预览的大小surfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());// 模板化一个Builder,下面是覆盖指定参数mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);mPreviewBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, CameraMetadata.CONTROL_CAPTURE_INTENT_VIDEO_SNAPSHOT);// 初始化参数mPreviewBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_OFF);// 边缘增强,高质量mPreviewBuilder.set(CaptureRequest.EDGE_MODE, CameraMetadata.EDGE_MODE_HIGH_QUALITY);// 3A--->automPreviewBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);// 3AmPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CameraMetadata.CONTROL_AF_MODE_AUTO);mPreviewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON);mPreviewBuilder.set(CaptureRequest.CONTROL_AWB_MODE, CameraMetadata.CONTROL_AWB_MODE_AUTO);// 输出到哪些SurfaceList<Surface> surfaces = new ArrayList<>();// Set up Surface for the camera previewSurface previewSurface = new Surface(surfaceTexture);surfaces.add(previewSurface);mPreviewBuilder.addTarget(previewSurface);// Set up Surface for the MediaRecorderSurface recorderSurface = mMediaRecorder.getSurface();surfaces.add(recorderSurface);mPreviewBuilder.addTarget(recorderSurface);// Start a capture session// Once the session starts, we can update the UI and start recordingmCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {@Overridepublic void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {Camera2Helper.this.mCameraCaptureSession = cameraCaptureSession;// 发送预览请求,开始预览,输出到上面的两个SurfacestartPreview();// 设置对焦的事件响应(监听),主要是设置对焦位置,然后重新发送一个预览请求mAutoFitTextureView.setmMyTextureViewTouchEvent(new TextureViewTouchEvent(mCameraCharacteristics, mAutoFitTextureView,mPreviewBuilder, Camera2Helper.this.mCameraCaptureSession, mPreviewRequest, mBackgroundHandler, mPreviewSessionCallback, mPreviewSize, mSensorOrientation));}@Overridepublic void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {Activity activity = mActivity;if (null != activity) {LogUtil.getInstance().e(activity, "Failed");}}}, mBackgroundHandler);} catch (CameraAccessException | IOException e) {e.printStackTrace();}}/*** 关闭预览的Session*/private void closePreviewSession() {if (mCameraCaptureSession != null) {mCameraCaptureSession.close();mCameraCaptureSession = null;}}/*** <p>开始预览</p>*/private void startPreview() {if (null == mCameraDevice) {return;}try {// 设置另外的预览参数,可以改地方,这里有点乱setUpCaptureRequestBuilder(mPreviewBuilder);mPreviewRequest = mPreviewBuilder.build();// 发送重复的请求,形成视频预览mCameraCaptureSession.setRepeatingRequest(mPreviewRequest, mPreviewSessionCallback, mBackgroundHandler);} catch (CameraAccessException e) {e.printStackTrace();}}
很关键的配置MediaRecoder的方法。
private void setupMediaRecorder() throws IOException {final Activity activity = mActivity;if (null == activity) {return;}// 无声模式,不设置源就行
// mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);// 源mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);// 封装格式mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);// 输出路径mMediaRecorder.setOutputFile(videoAbsolutePathTemp);mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight());mMediaRecorder.setVideoEncodingBitRate(10 * mVideoSize.getWidth() * mVideoSize.getHeight());mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);// I帧间隔,GOP的N值,而不是FPS!!!mMediaRecorder.setVideoFrameRate(20);
// mMediaRecorder.setMaxDuration(1000 * 10);mMediaRecorder.setOnErrorListener(new MediaRecorder.OnErrorListener() {@Overridepublic void onError(MediaRecorder mr, int what, int extra) {LogUtil.getInstance().e(TAG, "what:" + what + ", extra:" + extra);}});mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() {@Overridepublic void onInfo(MediaRecorder mr, int what, int extra) {LogUtil.getInstance().d(TAG, "what:" + what + ", extra:" + extra);if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) {activity.findViewById(R.id.videoRecord_btn_start).performClick();}}});// 设置旋转角度int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();switch (mSensorOrientation) {case SENSOR_ORIENTATION_DEFAULT_DEGREES:mMediaRecorder.setOrientationHint(DEFAULT_ORIENTATIONS.get(rotation));break;case SENSOR_ORIENTATION_INVERSE_DEGREES:mMediaRecorder.setOrientationHint(INVERSE_ORIENTATIONS.get(rotation));break;}// 必须preparemMediaRecorder.prepare();}
TextureView.SurfaceTextureListener
预览画面的SurfaceTexture状态回调。
/*** <p>预览View的Surface</p>*/private TextureView.SurfaceTextureListener mSurfaceTextureListener= new TextureView.SurfaceTextureListener() {/*** 可用时才能打开相机预览画面* @param width surfaceTextured的宽* @param height surfaceTexture的高*/@Overridepublic void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture,int width, int height) {openCamera(width, height);}/*** 大小改变时需要重新设置TextureView的Matrix参数* @param width surfaceTextured的宽* @param height surfaceTexture的高*/@Overridepublic void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture,int width, int height) {configureTransform(width, height);}/*** surfaceTexture销毁时调用*/@Overridepublic boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {LogUtil.getInstance().d(TAG, "SurfaceTexture被销毁");return true;}@Overridepublic void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {}};
涉及的方法
private void openCamera(int width, int height) {final Activity activity = mActivity;if (null == activity || activity.isFinishing()) {return;}try {// 设置形变和旋转Matrix参数configureTransform(width, height);// 打开相机mCameraManager.openCamera(cameraId, mStateCallback, null);} catch (CameraAccessException e) {LogUtil.getInstance().e(activity, "Cannot access the camera.");activity.finish();} catch (NullPointerException e) {new ErrorDialog(mActivity, mActivity.getString(R.string.camera_error)).onCreateDialog().show();}}
// 该方法不能在AutoFitTextureView大小固定之后或者相机打开之后调用,不生效!
private void configureTransform(int viewWidth, int viewHeight) {Activity activity = mActivity;if (null == mAutoFitTextureView || null == mPreviewSize || null == activity) {return;}int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();Matrix matrix = new Matrix();RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);RectF bufferRect = new RectF(0, 0, mPreviewSize.getHeight(), mPreviewSize.getWidth());float centerX = viewRect.centerX();float centerY = viewRect.centerY();if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) {bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY());matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL);float scale = Math.max((float) viewHeight / mPreviewSize.getHeight(),(float) viewWidth / mPreviewSize.getWidth());matrix.postScale(scale, scale, centerX, centerY);matrix.postRotate(90 * (rotation - 2), centerX, centerY);}mAutoFitTextureView.setTransform(matrix);}
录制视频
public void startRecordingVideo() {mIsRecordingVideo = true;mExecutorService.submit(new Runnable() {@Overridepublic void run() {// Start recordingmMediaRecorder.start();}});}
注:分配一个线程去记录视频,不会阻塞UI线程。
关闭相机
/*** 关闭相机设备*/private void closeCamera() {try {mCameraOpenCloseLock.acquire();closePreviewSession();if (null != mCameraDevice) {mCameraDevice.close();mCameraDevice = null;}if (null != mMediaRecorder) {mExecutorService.submit(new Runnable() {@Overridepublic void run() {mMediaRecorder.release();mMediaRecorder = null;}});}} catch (InterruptedException e) {throw new RuntimeException("Interrupted while trying to lock camera closing.");} finally {mCameraOpenCloseLock.release();}}
状态绑定
/*** 用于Activity的onResume状态*/public void onResume() {startBackgroundThread();if (mAutoFitTextureView.isAvailable()) {openCamera(mAutoFitTextureView.getWidth(), mAutoFitTextureView.getHeight());} else {mAutoFitTextureView.setSurfaceTextureListener(mSurfaceTextureListener);}}/*** 用于Activity的onPause状态*/public void onPause() {closeCamera();stopBackgroundThread();}/*** 用于Activity的onDestroy状态*/public void onDestroy() {closeCamera();stopBackgroundThread();}
总结
(1)视频录制,可以使用Camera 2或者Camera 1,Camera 1支持预览界面分辨率低,已经被抛弃。Camera 2支持的特性更多,分辨率更高,只是不能直接获取到原数据(可通过ImageReader获取到原数据)。
(2)此处使用MediaCoder进行录制,也可以使用ImageReader+MediaCodec(硬编码)进行录制(嫌麻烦,好像效率不太够,没实现)。
(3)也可以使用Surface缓存数据+(硬编码)进行进行录制,自己封装更灵活更细致的参数。
(4)需要将录制的状态和Activity的状态绑定。
(5)对焦要理解几个状态转换,以及构建预览请求的模板(CameraDevice.TEMPLATE_RECORD
可以触发连续对焦,而CameraDevice.TEMPLATE_PREVIEW
就不行),以及理解mPreviewBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, CameraMetadata.CONTROL_CAPTURE_INTENT_VIDEO_SNAPSHOT);
,后面的CONTROL_CAPTURE_INTENT_VIDEO_SNAPSHOT
的含义,看源码上面的注释,这个和模板化参数也有关系,主要就是这个INTENT
意图不同,不知道内部做了什么设置。
安卓MediaCodec硬编码实现
安卓MediaCodec硬编码实现
源码地址:https://github.com/shen511460468/MediaRecorderOnCamera2
Android项目小结——可对焦的视频录制(MediaRecorder与TextureView实现)相关推荐
- Android 基于MediaCodec+MediaMuxer实现音视频录制合成
AudioVideoCodec 一款视频录像机,支持AudioRecord录音.MediaCodec输出AAC.MediaMuxer合成音频视频并输出mp4,支持自动对焦.屏幕亮度调节.录制视频时长监 ...
- FFmpeg系列(二)-Android项目引入FFmpeg库播放视频
在系列一中讲述了如何编译FFmpeg的源码,现在就在Android项目中引入我们编译出来的库,并实现播放一个在线视频的功能 新建Android工程 新建一个支持ndk的Android工程,在AS中新建 ...
- 视频录制-MediaRecorder
视频录制: 首先视频的录制和音频的录制都是耗时的,需要在单个线程中去操作,在开启录制时,视频这块的录制,配置参数的前后,有严格的要求,颠倒会报错,报关于Camera,和MediaRecorder的错误 ...
- 【项目小结】某B视频网站的爬虫实践
最近忽来兴致,准备做评论数据的NLP项目.选定了某B视频网站的评论数据,顺带准备把某B视频网站的数据爬虫也一起做了.关于登录验证的问题可以看我的博客https://blog.csdn.net/CY19 ...
- Android视频录制-MediaRecorder流程
MediaRecorder流程如下: 当使用CameraSource的时候MediaCodecSource会从CameraSource中取数据. 当使用Surface的时候不用CameraSource ...
- Android项目小结——硬解码(MediaCodec实现[MP4]转YUV420各种格式)
YUV420 yuv420p:yv12(YYYYYYYY VV UU).I420(YYYYYYYY UU VV) yuv420sp:nv12(YYYYYYYY UV UV).nv21(YYYYYYYY ...
- Android项目小结---闹铃备忘录小开发知识点(附有:源码下载)
一.闹铃功能介绍: 1.为每条备忘添加闹铃 2.备忘内容和闹铃信息存在SQL中 3.可删除每天记录和闹铃或者删除所有 4.到点闹铃启动,包括锁屏时和重开机 5.在桌面的创建一个widget类似便签那样 ...
- android多媒体部分学习笔记八------音频录制 mediaRecorder
/** * 原始音频的播放和录制 * * audio * * audioTrack * * * @time 下午12:58:03 * @author retacn yue ...
- Android 使用CameraX实现预览/拍照/录制视频/图片分析/对焦/缩放/切换摄像头等操作
1. CameraX架构 看官方文档 CameraX架构 有如下这一段话 使用CameraX,借助名为"用例"的抽象概念与设备的相机进行交互. 预览 : 接受用于显示预览的Surf ...
最新文章
- 在微信小程序里自动获得当前手机所在的经纬度并转换成地址
- anasys hpc集群_这可能是最简单的并行方案,如何基于 AWS ParallelCluster 运行 ANSYS Fluent...
- final关键字_Java中的final关键字
- hdfs中与file数组类似的数组_如何在 JavaScript 中克隆数组
- 中山大学附属第一医院精准医学研究院 消化系统肿瘤研究于君课题组招聘启事...
- CNI portmap插件实现源码分析
- Win10系统下面的TR1008解决方案
- 开课吧9.9元学python靠谱吗-开课吧的python课程怎么样,值得报名吗?
- [mysqld_safe]centos7 mysql 安装与配置
- 二级java题型及分值_计算机二级java考试内容
- Geos库学习之(四)——几何对象空间关系判断实例
- 防止暴利破解,拒绝ip登陆
- oracle数据库什么情况下创建索引比较好
- linux 免费教程下载,Linux系统入门教程
- 让自由软件的风暴来的更猛烈吧!
- Android的16ms和垂直同步以及三重缓存
- 【2017年总结】-花开半夏
- java把汉字转换成拼音的2种方式
- “50份简历没获得面试”也正常
- 用DrawText实现高效的Android倒计时功能。
热门文章
- 格式化字符串漏洞(Format String Attack)
- Python基础数据结构之大循环(for list,set,dict,tuple)
- 在本地部署自己的漏洞文章武器库(详细步骤说明)
- 低学历计算机工作,非常适合低学历考生学习的5大技术,就业容易且高薪,可安身立命...
- 硬件设备接入企业微信调试面板
- Web服务器处理HTTP压缩之gzip、deflate压缩
- php简易验证码,PHP简易汉字验证码
- (附源码)springboot网络不良信息检查系统 毕业设计 231155
- 计算机硬件及相关设备基础知识,计算机硬件——基础知识(示例代码)
- 应届生直接做PM(产品经理)到底合不合适?