Created
December 6, 2017 14:48
-
-
Save spiritedRunning/8512b9da03fdee4ab8a54283281b1c22 to your computer and use it in GitHub Desktop.
Implement of Video Record through SurfaceView
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 视频录制自定义View | |
* | |
* @author zhen.liu | |
* @version 1.0 | |
* @date 2016/6/30 | |
*/ | |
public class VideoRecorderView extends LinearLayout implements MediaRecorder.OnErrorListener { | |
private static String TAG = "VideoRecordView"; | |
private Context mContext; | |
private SurfaceView mSurfaceView; | |
private SurfaceHolder mSurfaceHolder; | |
private MediaRecorder mMediaRecorder; | |
private Camera mCamera; | |
private Timer mTimer; | |
private OnRecordFinishListener mOnRecordFinishListener; | |
private OnRecordExceptionListener mOnRecordExceptionListener; | |
// 视频分辨率宽度 | |
private int mWidth; | |
// 视频分辨率高度 | |
private int mHeight; | |
// 是否打开camera | |
private boolean isOpenCamera = false; | |
// 一次拍摄最长时间 | |
private int mRecordMaxTime; | |
private int mTimeCount; | |
private File mRecordFile = null; | |
private int screenWidth; | |
private int screenHeight; | |
// 当前摄像头方向, 默认后置摄像头 | |
private int currentCamId = Camera.CameraInfo.CAMERA_FACING_BACK; | |
// 320 * 240, 640 * 480 | |
private static final int VIDEO_WIDTH = 640; | |
private static final int VIDEO_HEIGHT = 480; | |
// 是否在录制中 | |
public boolean isRecording = false; | |
public VideoRecorderView(Context context) { | |
this(context, null); | |
} | |
public VideoRecorderView(Context context, AttributeSet attrs) { | |
this(context, attrs, 0); | |
} | |
@SuppressLint("NewApi") | |
public VideoRecorderView(Context context, AttributeSet attrs, int defStyle) { | |
super(context, attrs, defStyle); | |
mContext = context; | |
mWidth = VIDEO_WIDTH; | |
mHeight = VIDEO_HEIGHT; | |
// 启动停止不算入录制时间 | |
mRecordMaxTime = Constants.MEDIA_MAX_DURATION + 2; | |
LayoutInflater.from(context).inflate(R.layout.movie_recorder_view, this); | |
mSurfaceView = (SurfaceView) findViewById(R.id.surfaceview); | |
screenWidth = DeviceUtil.getScreenWidth((Activity) mContext); | |
screenHeight = DeviceUtil.getScreenHeight((Activity) mContext); | |
LogUtil.d(TAG, "screenWidth: " + screenWidth + ", screenHeight: " + screenHeight); | |
// float videoRatio = (float)mWidth / (float)mHeight; | |
// float resizeHeight = screenWidth / videoRatio; | |
// | |
// ViewGroup.LayoutParams lp = mSurfaceView.getLayoutParams(); | |
// lp.height = (int)resizeHeight; | |
// LogUtil.d(TAG, "resize height: " + lp.height); | |
// mSurfaceView.setLayoutParams(lp); | |
mSurfaceHolder = mSurfaceView.getHolder(); | |
mSurfaceHolder.addCallback(new CustomCallBack()); | |
mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); | |
// 设置分辨率 | |
// mSurfaceHolder.setFixedSize(mWidth, mHeight); | |
// Display display = ((Activity) mContext).getWindowManager().getDefaultDisplay(); | |
// int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(display.getWidth(), MeasureSpec.UNSPECIFIED); | |
// int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(display.getHeight(), MeasureSpec.UNSPECIFIED); | |
// mSurfaceView.measure(childWidthMeasureSpec, childHeightMeasureSpec); | |
} | |
private class CustomCallBack implements SurfaceHolder.Callback { | |
@Override | |
public void surfaceCreated(SurfaceHolder holder) { | |
if (isOpenCamera) | |
return; | |
try { | |
LogUtil.i(TAG, "-----------surfaceCreated-----------"); | |
initCamera(); | |
isOpenCamera = true; | |
} catch (Exception e) { | |
mOnRecordExceptionListener.onRecordException(); | |
e.printStackTrace(); | |
} | |
} | |
@Override | |
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { | |
} | |
@Override | |
public void surfaceDestroyed(SurfaceHolder holder) { | |
if (!isOpenCamera) { | |
return; | |
} | |
freeCameraResource(); | |
} | |
} | |
/** | |
* 初始化摄像头 | |
*/ | |
public void initCamera() throws Exception { | |
if (mCamera != null) { | |
freeCameraResource(); | |
} | |
try { | |
mCamera = Camera.open(); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
freeCameraResource(); | |
} | |
if (mCamera == null) { | |
return; | |
} | |
setCamera(); | |
} | |
private void setCamera() throws Exception { | |
setCameraParams(); | |
int orientationDegree = setCameraDisplayOrientation((Activity) mContext, currentCamId); | |
mCamera.setDisplayOrientation(orientationDegree); | |
mCamera.setPreviewDisplay(mSurfaceHolder); | |
mCamera.startPreview(); | |
} | |
public int setCameraDisplayOrientation(Activity activity, int cameraId) { | |
android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo(); | |
android.hardware.Camera.getCameraInfo(cameraId, info); | |
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); | |
int degrees = 0; | |
switch (rotation) { | |
case Surface.ROTATION_0: | |
degrees = 0; | |
break; | |
case Surface.ROTATION_90: | |
degrees = 90; | |
break; | |
case Surface.ROTATION_180: | |
degrees = 180; | |
break; | |
case Surface.ROTATION_270: | |
degrees = 270; | |
break; | |
} | |
int result; | |
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { | |
result = (info.orientation + degrees) % 360; | |
result = (360 - result) % 360; // compensate the mirror | |
} else { // back-facing | |
result = (info.orientation - degrees + 360) % 360; | |
} | |
return result; | |
} | |
/** | |
* 设置摄像头摄像参数 | |
*/ | |
private void setCameraParams() throws RuntimeException { | |
int maxSizeWidth = 0; | |
int maxSizeHeight = 0; | |
boolean hasFitSize = false; | |
if (mCamera != null) { | |
// 部分手机未开拍摄权限 throw: java.lang.RuntimeException: Camera is being used after Camera.release() was called | |
Camera.Parameters params = mCamera.getParameters(); | |
// 设置自动对焦 | |
if (currentCamId == Camera.CameraInfo.CAMERA_FACING_BACK) { | |
List<String> focusMode = params.getSupportedFocusModes(); | |
if (focusMode.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { | |
LogUtil.d(TAG, "continuous-video mode"); | |
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); | |
} else { | |
LogUtil.d(TAG, "focus auto"); | |
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); | |
} | |
} | |
params.setRecordingHint(true); | |
params.set("orientation", "portrait"); | |
// 调整surfaceView显示比例适应手机屏幕 | |
List<Camera.Size> previewSizes = params.getSupportedPreviewSizes(); | |
for (int i = 0; i < previewSizes.size(); i++) { | |
Camera.Size pSize = previewSizes.get(i); | |
LogUtil.i(TAG, "phone supported width: " + pSize.width + ", height: " + pSize.height); | |
if ((pSize.width + pSize.height) > (maxSizeWidth + maxSizeHeight)) { | |
maxSizeWidth = pSize.width; | |
maxSizeHeight = pSize.height; | |
} | |
} | |
LogUtil.i(TAG, "maxSizeWidth: " + maxSizeWidth + ", maxSizeHeight: " + maxSizeHeight); | |
params.setPreviewSize(maxSizeWidth, maxSizeHeight); | |
// if (!hasFitSize) { | |
// Camera.Size optSize = getOptimalPreviewSize(mContext, previewSizes, mWidth, mHeight); | |
// if (optSize != null) { | |
// LogUtil.d(TAG, "previewSize: w = " + params.getPreviewSize().width + ", height = " + | |
// params.getPreviewSize().height); | |
// LogUtil.d(TAG, "optimalSize width: " + optSize.width + ", height: " + optSize.height); | |
// params.setPreviewSize(optSize.width, optSize.height); | |
// } | |
// } | |
mCamera.setParameters(params); | |
} | |
} | |
/** | |
* 根据目标尺寸获取手机最佳分辨率 | |
* | |
* @param sizes | |
* @param w | |
* @param h | |
* @return | |
*/ | |
public static Camera.Size getOptimalPreviewSize(Context mContext, List<Camera.Size> sizes, int w, int h) { | |
final double ASPECT_TOLERANCE = 0.05; | |
double targetRatio = (double) w / h; | |
LogUtil.d(TAG, "w: " + w + ", h: " + h + ", targetRatio: " + targetRatio); | |
if (sizes == null) { | |
return null; | |
} | |
Camera.Size optimalSize = null; | |
double minDiff = Double.MAX_VALUE; | |
// Because of bugs of overlay and layout, we sometimes will try to | |
// layout the viewfinder in the portrait orientation and thus get the | |
// wrong size of mSurfaceView. When we change the preview size, the | |
// new overlay will be created before the old one closed, which causes | |
// an exception. For now, just get the screen size | |
Display display = ((Activity) mContext).getWindowManager().getDefaultDisplay(); | |
int targetHeight = Math.min(display.getHeight(), display.getWidth()); | |
LogUtil.d(TAG, "display targetHeight: " + targetHeight); | |
if (targetHeight <= 0) { | |
WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); | |
targetHeight = windowManager.getDefaultDisplay().getHeight(); | |
LogUtil.d(TAG, "targetHeight2: " + targetHeight); | |
} | |
// Try to find an size match aspect ratio and size | |
for (Camera.Size size : sizes) { | |
double ratio = (double) size.width / size.height; | |
// LogUtil.d(TAG, "support width: " + size.width + ", height: " + size.height); | |
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) { | |
continue; | |
} | |
if (Math.abs(size.height - targetHeight) < minDiff) { | |
optimalSize = size; | |
minDiff = Math.abs(size.height - targetHeight); | |
LogUtil.d(TAG, "minDiff: " + minDiff); | |
} | |
} | |
// Cannot find the one match the aspect ratio, ignore the requirement | |
if (optimalSize == null) { | |
LogUtil.v(TAG, "No preview size match the aspect ratio"); | |
minDiff = Double.MAX_VALUE; | |
for (Camera.Size size : sizes) { | |
if (Math.abs(size.height - targetHeight) < minDiff) { | |
optimalSize = size; | |
minDiff = Math.abs(size.height - targetHeight); | |
} | |
} | |
} | |
return optimalSize; | |
} | |
/** | |
* 切换摄像头前后镜头 | |
*/ | |
public void changeCamera() { | |
try { | |
if (mCamera != null) { | |
freeCameraResource(); | |
} | |
int camIdx; | |
if (currentCamId == Camera.CameraInfo.CAMERA_FACING_BACK) { | |
camIdx = Camera.CameraInfo.CAMERA_FACING_FRONT; | |
} else { | |
camIdx = Camera.CameraInfo.CAMERA_FACING_BACK; | |
} | |
mCamera = Camera.open(camIdx); | |
currentCamId = camIdx; | |
if (mCamera == null) { | |
return; | |
} | |
setCamera(); | |
isOpenCamera = true; | |
} catch (Exception e) { | |
LogUtil.e(TAG, "changeCamera failed: " + e); | |
freeCameraResource(); | |
} | |
} | |
/** | |
* 修改闪光灯模式 | |
*/ | |
public String changeLightMode() { | |
if (mCamera != null) { | |
freeCameraResource(); | |
} | |
try { | |
mCamera = Camera.open(currentCamId); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
freeCameraResource(); | |
} | |
if (mCamera == null) { | |
return null; | |
} | |
Camera.Parameters parameters = mCamera.getParameters(); | |
if (parameters == null) { | |
return null; | |
} | |
List<String> flashModes = parameters.getSupportedFlashModes(); | |
if (flashModes == null) { | |
return null; | |
} | |
if (!flashModes.contains(Camera.Parameters.FLASH_MODE_TORCH) || | |
!flashModes.contains(Camera.Parameters.FLASH_MODE_OFF)) { | |
LogUtil.e(TAG, "not support flash mode"); | |
return null; | |
} | |
LogUtil.d(TAG, "current flash mode: " + parameters.getFlashMode()); | |
String mode = null; | |
if (parameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_TORCH)) { | |
mode = Camera.Parameters.FLASH_MODE_OFF; | |
} else { | |
mode = Camera.Parameters.FLASH_MODE_TORCH; | |
} | |
parameters.setFlashMode(mode); | |
try { | |
setCamera(); | |
isOpenCamera = true; | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
return mode; | |
} | |
/** | |
* 释放摄像头资源 | |
*/ | |
public void freeCameraResource() { | |
LogUtil.i(TAG, "====freeCameraResouce process===="); | |
try { | |
if (mCamera != null) { | |
mCamera.setPreviewCallback(null); | |
mCamera.stopPreview(); | |
mCamera.lock(); | |
mCamera.release(); | |
mCamera = null; | |
} | |
} catch (Exception e) { | |
LogUtil.e(TAG, "freeCameraResource exception: " + e); | |
} | |
isOpenCamera = false; | |
} | |
private void createRecordDir() { | |
//录制视频的保存地址 | |
File sampleDir = new File(Environment.getExternalStorageDirectory() + File.separator + "bee/video/"); | |
if (!sampleDir.exists()) { | |
sampleDir.mkdirs(); | |
} | |
File vecordDir = sampleDir; | |
try { | |
mRecordFile = File.createTempFile("recording", ".mp4", vecordDir); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
} | |
@SuppressLint("NewApi") | |
private void initRecord() throws IOException { | |
mMediaRecorder = new MediaRecorder(); | |
mMediaRecorder.reset(); | |
try { | |
// Step 1: Unlock and set camera to MediaRecorder | |
if (mCamera != null) { | |
mMediaRecorder.setCamera(mCamera); | |
} | |
// Step 2: Set sources before setOutputFormat() | |
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); | |
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); | |
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);// 视频输出格式 | |
// Step 3: Set video output format and encode | |
mMediaRecorder.setAudioEncodingBitRate(44100); | |
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); | |
CamcorderProfile mProfile = CamcorderProfile.get(CamcorderProfile.QUALITY_480P); | |
mMediaRecorder.setVideoSize(mWidth, mHeight);// 设置分辨率 | |
if (mProfile.videoBitRate > 2 * 1000 * 1000) { | |
mMediaRecorder.setVideoEncodingBitRate(1000 * 1000); // 320 * 240 | |
} else { | |
LogUtil.i(TAG, "set profile bitRate"); | |
mMediaRecorder.setVideoEncodingBitRate(mProfile.videoBitRate); | |
} | |
mMediaRecorder.setVideoFrameRate(30); | |
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); | |
// Step 4: Set output file | |
mMediaRecorder.setOutputFile(mRecordFile.getAbsolutePath()); | |
// Step 5: Set the preview output | |
mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface()); | |
// 调整录制角度 | |
if (currentCamId == Camera.CameraInfo.CAMERA_FACING_FRONT) { | |
mMediaRecorder.setOrientationHint(270); | |
} else { | |
int orientationDegree = setCameraDisplayOrientation((Activity) mContext, currentCamId); | |
mMediaRecorder.setOrientationHint(orientationDegree); | |
} | |
mMediaRecorder.setOnErrorListener(this); | |
// mMediaRecorder.setMaxDuration(Constant.MAXVEDIOTIME * 1000); | |
mMediaRecorder.prepare(); | |
mMediaRecorder.start(); | |
} catch (IllegalStateException e) { | |
e.printStackTrace(); | |
} catch (RuntimeException e) { | |
e.printStackTrace(); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
} | |
/** | |
* 开始录制视频 | |
* | |
* @param onRecordFinishListener 达到指定时间之后回调接口 | |
*/ | |
public void record(OnRecordFinishListener onRecordFinishListener) { | |
this.mOnRecordFinishListener = onRecordFinishListener; | |
createRecordDir(); | |
isRecording = true; | |
try { | |
if (!isOpenCamera) { | |
LogUtil.i(TAG, "start record initCamera-----------"); | |
initCamera(); | |
} | |
if (mCamera != null) { | |
// 解决HTC录制花屏 | |
if (DeviceUtil.isSpecialDevice(DeviceUtil.DEVICE_HTC)) { | |
mCamera.stopPreview(); | |
} | |
mCamera.unlock(); | |
} | |
initRecord(); | |
mTimeCount = 0; | |
mTimer = new Timer(); | |
mTimer.schedule(new TimerTask() { | |
@Override | |
public void run() { | |
++mTimeCount; | |
LogUtil.d(TAG, "mTimecount = " + mTimeCount); | |
if (mTimeCount == mRecordMaxTime) { | |
if (mOnRecordFinishListener != null) | |
mOnRecordFinishListener.onRecordFinish(); | |
} | |
} | |
}, 0, 1000); | |
} catch (Exception e) { | |
mOnRecordExceptionListener.onRecordException(); | |
e.printStackTrace(); | |
} | |
} | |
/** | |
* 停止拍摄 | |
*/ | |
public void stop() { | |
LogUtil.i(TAG, "VideoRecord set stop------------"); | |
stopRecord(); | |
} | |
/** | |
* 停止录制, 释放资源 | |
*/ | |
public void stopRecord() { | |
LogUtil.i(TAG, "=====stopRecord===="); | |
if (mTimer != null) { | |
mTimer.cancel(); | |
} | |
if (mMediaRecorder != null) { | |
// 防止crash | |
mMediaRecorder.setOnErrorListener(null); | |
mMediaRecorder.setOnInfoListener(null); | |
mMediaRecorder.setPreviewDisplay(null); | |
try { | |
mMediaRecorder.stop(); | |
mMediaRecorder.release(); | |
} catch (IllegalStateException e) { | |
e.printStackTrace(); | |
} catch (RuntimeException e) { | |
e.printStackTrace(); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
mMediaRecorder = null; | |
} | |
} | |
public int getTimeCount() { | |
return mTimeCount; | |
} | |
public File getRecordFile() { | |
return mRecordFile; | |
} | |
/** | |
* 录制完成回调接口 | |
*/ | |
public interface OnRecordFinishListener { | |
void onRecordFinish(); | |
} | |
public interface OnRecordExceptionListener { | |
void onRecordException(); | |
} | |
public void setOnExceptionListener(OnRecordExceptionListener listener) { | |
this.mOnRecordExceptionListener = listener; | |
} | |
@Override | |
public void onError(MediaRecorder mr, int what, int extra) { | |
try { | |
if (mr != null) { | |
mr.reset(); | |
} | |
} catch (IllegalStateException e) { | |
e.printStackTrace(); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment