Skip to content

Instantly share code, notes, and snippets.

@spiritedRunning
Created December 6, 2017 14:48
Show Gist options
  • Save spiritedRunning/8512b9da03fdee4ab8a54283281b1c22 to your computer and use it in GitHub Desktop.
Save spiritedRunning/8512b9da03fdee4ab8a54283281b1c22 to your computer and use it in GitHub Desktop.
Implement of Video Record through SurfaceView
/**
* 视频录制自定义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