先上效果图,本文只摘取部分代码,
完整代码请戳
https://github.com/ycy726619/WaterRecord
1.全屏预览 首先先创建一个RecordCameraView继承于TextureView
2.视频录制 开始录制准备工作
3.给本地视频添加水印 这里我采用的是ffmpeg给视频添加水印,因为时间关系使用了github一位大神编译好的ffmpeg库,如果有兴趣的大佬,闲暇时间可以研究一下怎么在Android编译并使用ffmpeg。 ffmpeg依赖地址:com.coder.command:ffmpeg:1.1.6
1.全屏预览 首先先创建一个RecordCameraView继承于TextureView
public class RecordCameraView extends TextureView {}
构造函数
public RecordCameraView(Context context) { this(context, null); } public RecordCameraView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public RecordCameraView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initView(context); }
测量预览宽高,这里如果是width < height * mRatioWidth / mRatioHeight那么预览的就是实际拍摄画面。
@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 { //全屏预览 width > height * mRatioWidth / mRatioHeight if (width > height * mRatioWidth / mRatioHeight) { setMeasuredDimension(width, width * mRatioHeight / mRatioWidth); } else { setMeasuredDimension(height * mRatioWidth / mRatioHeight, height); } }}
预览线程
private void startBackgroundThread() { mBackgroundThread = new HandlerThread("CameraBackground"); mBackgroundThread.start(); mBackgroundHandler = new Handler(mBackgroundThread.getLooper());}
打开相机
private void openCamera(int width, int height) { CameraManager manager = (CameraManager) getContext().getSystemService(Context.CAMERA_SERVICE); try { Log.e(TAG, "tryAcquire"); if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) { throw new RuntimeException("锁定相机开启时间已过。"); } String cameraId = manager.getCameraIdList()[0]; CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); StreamConfigurationMap map = characteristics .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); if (map == null) { throw new RuntimeException("无法获得可用的预览/视频大小"); } mVideoSize = chooseVideoSize(map.getOutputSizes(MediaRecorder.class)); mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class), width, height, mVideoSize); int orientation = getResources().getConfiguration().orientation; if (orientation == Configuration.ORIENTATION_LANDSCAPE) { mTextureView.setAspectRatio(mPreviewSize.getWidth(), mPreviewSize.getHeight()); } else { mTextureView.setAspectRatio(mPreviewSize.getHeight(), mPreviewSize.getWidth()); } configureTransform(width, height); mMediaRecorder = new MediaRecorder(); manager.openCamera(cameraId, mStateCallback, null); } catch (CameraAccessException e) { Log.e(TAG, "无法进入相机"); } catch (NullPointerException e) { Log.e(TAG, "此设备不支持Camera2 API"); } catch (InterruptedException e) { throw new RuntimeException("试图锁定相机打开时被打断"); }}
相机状态回调
private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice cameraDevice) { mCameraDevice = cameraDevice; startPreview(); mCameraOpenCloseLock.release(); if (null != mTextureView) configureTransform(mTextureView.getWidth(), mTextureView.getHeight()); } @Override public void onDisconnected(@NonNull CameraDevice cameraDevice) { mCameraOpenCloseLock.release(); cameraDevice.close(); mCameraDevice = null; } @Override public void onError(@NonNull CameraDevice cameraDevice, int error) { mCameraOpenCloseLock.release(); cameraDevice.close(); mCameraDevice = null; } };
开始预览
private void startPreview() { if (null == mCameraDevice || !isAvailable() || null == mPreviewSize) { return; } try { closePreviewSession(); SurfaceTexture texture = mTextureView.getSurfaceTexture(); assert texture != null; texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); Surface previewSurface = new Surface(texture); mPreviewBuilder.addTarget(previewSurface); mCameraDevice.createCaptureSession(Collections.singletonList(previewSurface), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { mPreviewSession = session; updatePreview(); } @Override public void onConfigureFailed(@NonNull CameraCaptureSession session) { if (null != activity) { Toast.makeText(activity, "Failed", Toast.LENGTH_SHORT).show(); } } }, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }
设置预览宽高
private void configureTransform(int viewWidth, int viewHeight) { if (null == mTextureView || 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); } mTextureView.setTransform(matrix); }
2.视频录制 开始录制准备工作
public void startRecordingVideo() { if (null == mCameraDevice || !mTextureView.isAvailable() || null == mPreviewSize) { return; } try { closePreviewSession(); setUpMediaRecorder(); SurfaceTexture texture = mTextureView.getSurfaceTexture(); assert texture != null; texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); List surfaces = new ArrayList<>(); Surface previewSurface = new Surface(texture); surfaces.add(previewSurface); mPreviewBuilder.addTarget(previewSurface); Surface recorderSurface = mMediaRecorder.getSurface(); surfaces.add(recorderSurface); mPreviewBuilder.addTarget(recorderSurface); mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) { mPreviewSession = cameraCaptureSession; updatePreview(); activity.runOnUiThread(() -> mMediaRecorder.start()); } @Override public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { } }, mBackgroundHandler); } catch (CameraAccessException | IOException e) { e.printStackTrace(); } }
配置MediaRecorder参数
private void setUpMediaRecorder() throws IOException { mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); //视频输出格式 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); if (mNextVideoAbsolutePath == null || mNextVideoAbsolutePath.isEmpty()) { mNextVideoAbsolutePath = getVideoFilePath(activity); } //录制输出路径 mMediaRecorder.setOutputFile(mNextVideoAbsolutePath); //视频比特率 越大录制越清晰 文件也会变大 mMediaRecorder.setVideoEncodingBitRate(800*1024); //帧率 mMediaRecorder.setVideoFrameRate(30); //视频录制宽高 mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight()); //视频编码 mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); //音频编码 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); //手机角度 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; } mMediaRecorder.prepare(); }
结束录制
public void stopRecordingVideo() { if (mMediaRecorder != null) { mMediaRecorder.stop(); mMediaRecorder.reset(); Log.e(TAG, "Video saved: " + mNextVideoAbsolutePath); startPreview(); } }
使用方式
xml
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.example.myapplication.util.RecordCameraView android:id="@+id/recordCameraView" android:layout_width="match_parent" android:layout_height="match_parent"/> <TextView android:id="@+id/tvRecord" android:layout_alignParentBottom="true" android:layout_margin="16dp" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="15dp" android:gravity="center" android:textStyle="bold" android:textSize="18sp" android:textColor="@color/white" android:background="@drawable/tv_record_bg" android:text="开始录制"/>RelativeLayout>
activity
private RecordCameraView recordCameraView; private TextView tvRecord; private boolean isRecord = false; private long record; private String outPutPath = ""; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); recordCameraView = findViewById(R.id.recordCameraView); tvRecord = findViewById(R.id.tvRecord); tvRecord.setOnClickListener(v -> { if (System.currentTimeMillis() - record < 2000) { Toast.makeText(MainActivity.this, "录制时间太短!", Toast.LENGTH_SHORT).show(); return; } if (isRecord) { isRecord = false; recordCameraView.stopRecordingVideo(); } else { isRecord = true; record = System.currentTimeMillis(); recordCameraView.startRecordingVideo(); tvRecord.setText("停止录制"); } }); } @Override protected void onPause() { super.onPause(); recordCameraView.onPause(); } @Override protected void onResume() { super.onResume(); recordCameraView.onResume(); }
3.给本地视频添加水印 这里我采用的是ffmpeg给视频添加水印,因为时间关系使用了github一位大神编译好的ffmpeg库,如果有兴趣的大佬,闲暇时间可以研究一下怎么在Android编译并使用ffmpeg。 ffmpeg依赖地址:com.coder.command:ffmpeg:1.1.6
implementation 'com.coder.command:ffmpeg:1.1.6'
这个库中对于常见的ffmpeg命令已经封装好了,对于一些特殊要求还是得自己来写ffmpeg命令,当前库中所提供的方法如下。
FFmpegUtils->transformAudio(String srcFile, String targetFile) // 音频转码FFmpegUtils->transformVideo(String srcFile, String targetFile) // 视频转码FFmpegUtils->cutAudio(String srcFile, int startTime, int duration, String targetFile) //音频剪切FFmpegUtils->cutVideo(String srcFile, int startTime, int duration, String targetFile) //视频剪切FFmpegUtils->concatAudio(String srcFile, String appendFile, String targetFile) //音频拼接FFmpegUtils->concatVideo(String inputFile, String targetFile) // 视频拼接FFmpegUtils->extractAudio(String srcFile, String targetFile) //音频抽取FFmpegUtils->extractVideo(String srcFile, String targetFile) //视频抽取FFmpegUtils->mixAudioVideo(String videoFile, String audioFile, int duration, String muxFile) //音视频合成FFmpegUtils->screenShot(String srcFile, String targetFile) //截取视频第一帧FFmpegUtils->video2Image(String inputFile, String targetDir,@ImageFormat String format) // 视频转图片FFmpegUtils->video2Gif(String srcFile, int startTime, int duration, String targetFile) //视频转gifFFmpegUtils->decodeAudio(String srcFile, String targetFile, int sampleRate, int channel) //音频解码pcmFFmpegUtils->decode2YUV(String srcFile, String targetFile) //视频解码YUVFFmpegUtils->image2Video(String srcDir, @ImageFormat String format, String targetFile) //图片转视频FFmpegUtils->addWaterMark(String srcFile, String waterMark, String targetFile) //添加视频水印FFmpegUtils->encodeAudio(String srcFile, String targetFile, int sampleRate,int channel) //音频编码FFmpegUtils->yuv2H264(String srcFile, String targetFile,int width, int height) //视频编码H264FFmpegUtils->multiVideo(String input1, String input2, String targetFile, @Direction int direction) //多画面拼接FFmpegUtils->reverseVideo(String inputFile, String targetFile) //反向播放FFmpegUtils->videoDoubleDown(String srcFile, String targetFile) //视频缩小一倍FFmpegUtils->videoDoubleUp(String srcFile, String targetFile) //视频放大一倍FFmpegUtils->videoSpeed2(String srcFile, String targetFile) //倍速播放FFmpegUtils->denoiseVideo(String inputFile, String targetFile) //视频降噪FFmpegUtils->audioFadeIn(String srcFile, String targetFile) //音频淡入FFmpegUtils->audioFadeOut(String srcFile, String targetFile, int start, int duration) //音频淡出FFmpegUtils->videoBright(String srcFile, String targetFile, float bright) //修改视频亮度FFmpegUtils->videoContrast(String srcFile, String targetFile, float contrast) //修改视频对比度FFmpegUtils->picInPicVideo(String inputFile1, String inputFile2, int x, int y, String targetFile) //画中画FFmpegUtils->videoScale(String srcFile, String targetFile, int width, int height) //视频固定缩放FFmpegUtils->audio2Fdkaac(String srcFile,String targetFile) //音频-fdk_aacFFmpegUtils->audio2Mp3lame(String srcFile,String targetFile) // 音频-mp3lameFFmpegUtils->video2HLS(String srcFile, String targetFile,int splitTime) //视频切片FFmpegUtils->hls2Video(String m3u8Index,String targetFile); // 切片转视频
就比如这里给视频添加水印,使用库中所提供的方法最终图片水印会被绘制在视频的左上方。
public static String[] addWaterMark(String srcFile, String waterMark, String targetFile) { String command = "ffmpeg -y -i %s -i %s -filter_complex overlay=40:40 %s"; command = String.format(command, srcFile, waterMark, targetFile); return command.split(" ");//以空格分割为字符串数组 }
如果需要让图片水印显示在左下方呢,或者显示在其他位置怎么搞?代码如下:
private static String[] getWaterCommands(String videoUrl, String imageUrl, String outputUrl) { String[] commands = new String[9]; commands[0] = "ffmpeg"; //输入 commands[1] = "-i"; commands[2] = videoUrl; //水印 commands[3] = "-i"; commands[4] = imageUrl; commands[5] = "-filter_complex"; //水印位置 main_w-overlay_w 视频宽度 | main_h-overlay_h 视频高度 commands[6] = "overlay=20:(main_h-overlay_h)-20"; //覆盖输出 //直接覆盖输出文件 commands[7] = "-y"; //输出文件 commands[8] = outputUrl; return commands; }
添加水印工具类如下,使用时请注意修改水印图片名称及位置,以及在添加水印前确保水印图片本地文件存在。
public class WaterMarkUtils { static String WATER_NAME = "water_mark.png"; static String WATER_MARK_PATH = Environment.getExternalStoragePublicDirectory("") + "/waterMark/"; public static void addWaterMark(final String srcFile, final String outPutFile, final IFFmpegCallBack callBack) { FFmpegCommand.runAsync(getWaterCommands(srcFile, getWaterPath(), outPutFile), callBack); } public static String getWaterPath() { return WATER_MARK_PATH + WATER_NAME; } public static String getVideoOutPutFilePath(Context context) { final File dir = context.getExternalFilesDir(null); return (dir == null ? "" : (dir.getAbsolutePath() + "/output")) + System.currentTimeMillis() + ".mp4"; } @SuppressLint("ResourceType") public static void initWaterFile(Context context) { Resources r = context.getResources(); InputStream is = r.openRawResource(R.drawable.water_mark); BitmapDrawable bmpDraw = new BitmapDrawable(is); Bitmap bmp = bmpDraw.getBitmap(); File dir = new File(WATER_MARK_PATH); if (!dir.exists()) { dir.mkdirs(); } File file = new File(WATER_MARK_PATH, WATER_NAME); try { if (file.createNewFile()) { try { BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file)); bmp.compress(Bitmap.CompressFormat.PNG, 100, bos); bos.flush(); bos.close(); } catch (IOException e) { e.printStackTrace(); } } } catch (IOException e) { e.printStackTrace(); } } /** * 添加水印ffmpeg命令 * * @param videoUrl * @param imageUrl * @param outputUrl * @return */ private static String[] getWaterCommands(String videoUrl, String imageUrl, String outputUrl) { String[] commands = new String[9]; commands[0] = "ffmpeg"; //输入 commands[1] = "-i"; commands[2] = videoUrl; //水印 commands[3] = "-i"; commands[4] = imageUrl; commands[5] = "-filter_complex"; //水印位置 main_w-overlay_w 视频宽度 | main_h-overlay_h 视频高度 commands[6] = "overlay=20:(main_h-overlay_h)-20"; //覆盖输出 //直接覆盖输出文件 commands[7] = "-y"; //输出文件 commands[8] = outputUrl; return commands; }}
添加水印使用
private final String TAG = "MainActivityTAG"; private final int CANCEL = 100; private final int ERROR = 101; private final int COMPLETE = 102; private final int START = 103; private final Handler handler = new Handler() { @Override public void handleMessage(@NonNull Message msg) { super.handleMessage(msg); switch (msg.what) { case CANCEL: case ERROR: //添加水印异常 break; case COMPLETE: //水印添加完成 break; case START: //水印添加开始 break; } } }; private void startAddWaterMaker() { handler.sendEmptyMessage(START); outPutPath = WaterMarkUtils.getVideoOutPutFilePath(MainActivity.this); String path = recordCameraView.getVideoAbsolutePath(); if (TextUtils.isEmpty(path)) { Toast.makeText(MainActivity.this,"视频文件保存失败",Toast.LENGTH_SHORT).show(); return; } WaterMarkUtils.addWaterMark(path, outPutPath, new IFFmpegCallBack() { @Override public void onCancel() { Log.e(TAG, "onCancel: "); handler.sendEmptyMessage(CANCEL); } @Override public void onError(Throwable t) { Log.e(TAG, "onError: " + t.getMessage()); handler.sendEmptyMessage(ERROR); } @Override public void onProgress(int progress) { Log.e(TAG, "onProgress: progress = " + progress); } @Override public void onComplete() { Log.e(TAG, "onComplete: "); handler.sendEmptyMessage(COMPLETE); } @Override public void onStart() { Log.e(TAG, "onStart: "); } }); }
文末顺便提一下,Android使用ffmpeg来操作视频耗时很久,一般不会应用到线上,举个例子,帧率24 比特率800kb/s 录制时长15s,测试设备红米10x,给当前视频添加水印大概耗时为25s-40s之间,所以当大佬们想使用ffmpeg操作视频得注意下奥。