ffmpeg 水印_Camera2全屏预览,视频录制及使用ffmpeg给本地视频添加水印

该博客介绍如何利用ffmpeg库,在视频录制完成后,将水印添加到视频的左上方,实现全屏预览和视频录制功能。
摘要由CSDN通过智能技术生成
先上效果图,本文只摘取部分代码, 完整代码请戳 https://github.com/ycy726619/WaterRecord

c2740c586099b23dfb8e874396891ade.png


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操作视频得注意下奥。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值