uniapp 原生插件 Android拍摄视频添加动态时间水印

基于动态时间水印的生成

如果只是给图片或视频添加普通的静态水印,功能很简单,可以直接用FFMPEG,或者自己通过Bitmap、Canvas实现图片添加水印,但想要像监控视频上那种读秒的时间水印就比较困难,基于客户需求,开发了一套IOS+Android的原生水印相机插件实现了这个功能,分享一下代码和心得。

原理:通过camera api1的onPreviewFrame(byte[] data, Camera camera)方法得到预览时每一帧的数据,通过mediacoder编码(在这个过程中给每一帧添加水印),后传给MediaMuxer进行混合,生成mp4视频文件

java 复制代码
 @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        if(isRecording){
            // Video处理类会维护一个容器存放每一帧的待处理数据,如果这里没有做缓存处理或者延时处理将会内存爆炸
            if(data != null){
//                Bitmap waterImage = null;
                if(isChangeWaterMark){
                    UniLogUtils.e("水印内容改变了,重新生成水印Bitmap");
                    isChangeWaterMark = false;
                    if(mWaterMarkRootView.getChildCount() > 0){
                        lastWaterMark = ImageUtil.viewToBitmap4Video(mWaterMarkRootView);
                    }
                }

                MediaMuxerThread.addVideoFrameData(data, lastWaterMark);

                // 断层的原因 : buffer数组可能在混合器还没处理完时又重新覆盖了新的数据
                // 现在用多个缓存区来交替,这样某个缓存区在上一帧来不及处理时不至于立刻被下一帧的数据覆盖,而是使用下一个缓存区
                camera.addCallbackBuffer(getCacheBuffer());
            }
        }else{
            camera.addCallbackBuffer(getCacheBuffer(0));
        }
    }

使用addCallbackBuffer方法,让内存复用来提高预览的效率,但是如果没有调用这个方法addCallbackBuffer(byte[]),帧回调函数(onPreviewFrame)就不会被调用

camera.addCallbackBuffer(getCacheBuffer());

getCacheBuffer()是一个缓存buffer,用于在多线程处理时,避免上一帧数据还未处理完就被覆盖了,导致花屏

视频桢和水印处理线程

java 复制代码
package io.dcloud.uniplugin;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.hardware.Camera;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.renderscript.Allocation;
import android.util.Log;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import androidx.core.util.Pools;
import io.dcloud.feature.uniapp.utils.UniLogUtils;
import io.dcloud.uniplugin.entity.EncoderEntity;
import io.dcloud.uniplugin.entity.BasePool;
import io.dcloud.uniplugin.entity.EncoderPool;
import io.dcloud.uniplugin.util.SYUtils;

/**
 * 视频编码线程
 */
@SuppressWarnings("deprecation")
public class VideoEncoderThread extends Thread {
    // 编码相关参数
    private static final String MIME_TYPE = "video/avc"; // H.264 Advanced Video
//    private static final String MIME_TYPE = "video/hevc"; // H.265 Advanced Video
    // 这里帧率和关键帧的数值很重要,之前因为帧率设置为30,关键帧设置10、15、20都会存在一定帧的花屏,因为实际帧率可能是29.8或29
    // 所以这里主动降低帧率到28,固定关键帧间隔为14 (一般关键帧为帧率的一半)
    private static final int FRAME_RATE = 28; // 帧率
    private static final int IFRAME_INTERVAL = 14; // I帧间隔(GOP)
    private static final int TIMEOUT_USEC = 10000; // 编码超时时间

    private volatile EncoderPool encoderPool = new EncoderPool();
    private byte[] nv21;
    private int[] argb;
    private byte[] output;

    // 存储每一帧的数据 Vector 自增数组
    private volatile Vector<EncoderEntity> frameBytes;
    private byte[] mFrameData;

    private static int BIT_RATE;
    private final Object lock = new Object();

    private MediaCodec mMediaCodec;  // Android硬编解码器
    private MediaCodec.BufferInfo mBufferInfo; //  编解码Buffer相关信息

    private WeakReference<MediaMuxerThread> mediaMuxer; // 音视频混合器
    private MediaFormat mediaFormat; // 音视频格式

    private volatile boolean isStart = false;
    private volatile boolean isExit = false;
    private volatile boolean isMuxerReady = false;
    private int length;

    private final Pools.Pool<MyCallBack> mRunnablePool;

    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    ExecutorService scheduler;
    private static final int MAX_POOL_SIZE = 20;

    public int getW(){
        return App.previewWidth;
    }

    public int getH(){
        return App.previewHeight;
    }

    public MyCallBack acquire(EncoderEntity encoder) {
        MyCallBack instance = mRunnablePool.acquire();
        if (instance == null) {
            instance = new MyCallBack(encoder);
        }else{
            instance.encoder = encoder;
        }

        return instance;
    }

    public void release(MyCallBack instance) {
        mRunnablePool.release(instance);
    }

    public VideoEncoderThread(WeakReference<MediaMuxerThread> mediaMuxer) {
        mRunnablePool = new Pools.SynchronizedPool<>(MAX_POOL_SIZE);

        // 初始化相关对象和参数
        this.mediaMuxer = mediaMuxer;
        frameBytes = new Vector<>();

        scheduler = Executors.newFixedThreadPool(4);

        length = getW() * getH() * 3 / 2;
        mFrameData = new byte[length];

        nv21 = new byte[getH() * getW() *3 / 2];
        argb = new int[getH() * getW()];
        output = new byte[getH() * getW() + getH() * getW() / 2];

        // 文件会模糊是因为比特率的值太小了
//        BIT_RATE = getH() * getW() * 5;
        BIT_RATE = getH() * getW() * 8;
        paint.setColor(Color.WHITE);
        paint.setTextSize(25);
        prepare();
    }

    // 执行相关准备工作
    private void prepare() {
        mBufferInfo = new MediaCodec.BufferInfo();
        mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE,getH(),getW());
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);

        mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
//        BITRATE_MODE_CQ: 表示完全不控制码率,尽最大可能保证图像质量
//        BITRATE_MODE_CBR: 表示编码器会尽量把输出码率控制为设定值
//        BITRATE_MODE_VBR: 表示编码器会根据图像内容的复杂度(实际上是帧间变化量的大小)来动态调整输出码率,图像复杂则码率高,图像简单则码率低;

        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
        // 自动触发实际是按照帧数触发的,例如设置帧率为 20 fps,关键帧间隔为 1s ,那就会每 20桢输出一个关键帧,一旦实际帧率低于配置帧率,那就会导致关键帧间隔时间变长。
        // 由于 MediaCodec 启动后就不能修改配置帧率/关键帧间隔了,所以如果希望改变关键帧间隔帧数,就必须重启编码器。
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
    }

    /**
     * 开始视频编码
     */
    private void startMediaCodec() throws IOException {
        mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
        mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mMediaCodec.start();
        isStart = true;
    }

    //混合器已经初始化,等待添加轨道
    public void setMuxerReady(boolean muxerReady) {
        synchronized (lock) {
            Log.e("=====视频录制", Thread.currentThread().getId() + " video -- setMuxerReady..." + muxerReady);
            isMuxerReady = muxerReady;
            lock.notifyAll();
        }
    }

    public void add(byte[] data, Bitmap waterMark) {
        UniLogUtils.e("子线程 添加到待处理队列中!!!"+isMuxerReady+":"+isStart);

        if(data.length != length){
            UniLogUtils.e("遇到不一样长度的了: "+"长度:"+length+"  实际长度:"+data.length);
            return;
        }

        if (frameBytes != null && isMuxerReady && isStart) {
            EncoderEntity encoder = new EncoderEntity(); //encoderPool.acquireEncoder();
//            EncoderEntity encoder = encoderPool.acquire();
            encoder.data = data;
            encoder.waterMark = waterMark;

            frameBytes.add(encoder);

            scheduler.execute(acquire(encoder));
//            scheduler.execute(new MyCallBack(encoder));
        }
    }

    private class MyCallBack implements Runnable{
        private volatile EncoderEntity encoder;

        MyCallBack(EncoderEntity encoder){
            this.encoder = encoder;
        }

        @Override
        public void run() {
            try {
                if(!isExit){
                    if(isStart){
                        encoder.data = dealByteWait(encoder);
                        encoder.valid = true;
                        UniLogUtils.e("子线程处理完毕并添加到主队列"+Thread.currentThread().getName());
                        Thread.sleep(5);
                    }
                }
            }catch (InterruptedException e1) {
            }finally {
                release(this);
            }
        }
    }

    public int sizeOfFrame(){
        if(frameBytes == null){
            return -1;
        }
        return frameBytes.size();
    }

    @Override
    public void run() {
        super.run();
        while (!isExit) {
            if (!isStart) {
                stopMediaCodec();
                if (!isMuxerReady) {
                    synchronized (lock) {
                        try {
                            Log.e("=====视频录制", "video -- 等待混合器准备...");
                            lock.wait();
                        } catch (InterruptedException e) {
                        }
                    }
                }

                if (isMuxerReady) {
                    try {
                        Log.e("=====视频录制", "video -- startMediaCodec...");
                        startMediaCodec();
                    } catch (IOException e) {
                        isStart = false;
                        try {
                            Thread.sleep(5);
                        } catch (InterruptedException e1) {
                        }
                    }
                }
            } else if (!frameBytes.isEmpty()) {
                EncoderEntity item = frameBytes.get(0);
                if(item != null && item.valid){
                    frameBytes.remove(0);
                    UniLogUtils.e("宿主子线程 帧数据总元素:"+frameBytes.size()+"  isExit="+isExit);
                    encodeFrame(item);
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else{
                if(isStart){
                    UniLogUtils.e("子线程 库存已经取用完 ");
                    try {
                        Thread.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        Log.e("=====视频录制", "Video 录制线程 退出...");
    }

    public void exit() {
        isExit = true;
    }

    /**
     * 将拿到的预览帧数据转为bitmap添加水印 再将bitmap转为帧数据
     * @param encoder 预览的帧数据
     * @return
     */
    private byte[] dealByteWait(EncoderEntity encoder) {
        byte[] sss;

        if(encoder.data == null){
            UniLogUtils.e("遇到了空的数据");
            return null;
        }

        if(BaseCamera.cameraType == Camera.CameraInfo.CAMERA_FACING_BACK){
            sss = SYUtils.rotateYUV420Degree90(encoder.data, output, getW(),getH());
        }else{
            sss = SYUtils.rotateYUV420Degree270(encoder.data, output, getW(),getH());
        }

        Bitmap bitmapAll = Bitmap.createBitmap(getH(), getW(), Bitmap.Config.ARGB_8888);//.copy(Bitmap.Config.ARGB_8888,true);
        Canvas canvas = new Canvas(bitmapAll);

        Allocation out = LuanQingCamera.myClass.nv21ToBitmap(sss, getH(), getW());
        if(out == null){
            return null;
        }
        out.copyTo(bitmapAll);

        if(encoder.waterMark != null){
            canvas.drawBitmap(encoder.waterMark,0,0, paint);
        }

        byte[] returnData = SYUtils.bitmapToNv21(bitmapAll , bitmapAll.getWidth(), bitmapAll.getHeight() , nv21, argb);
        bitmapAll.recycle();
        canvas.setBitmap(null);

        if (returnData != null) {
            return returnData;
        } else {
            return null;
        }
    }

    /**
     * 编码每一帧的数据
     *
     * @param item 每一帧的数据
     */
    private void encodeFrame(EncoderEntity item) {
        if(item.data == null){
            return;
        }

        // 将原始的N21数据转为I420
        SYUtils.NV21toI420SemiPlanar(item.data, mFrameData, getW(), getH());

        // 销毁掉帧对象 START
        if(item != null){
            item.data = null;
            encoderPool.release(item);
        }
        // 销毁掉帧对象 END

        ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
        ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();

        int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC);
        if (inputBufferIndex >= 0) {
            ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
            inputBuffer.clear();
            inputBuffer.put(mFrameData);

            mMediaCodec.queueInputBuffer(inputBufferIndex, 0, mFrameData.length, System.nanoTime() / 1000, isExit ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
        } else {
            Log.e("=====视频录制", "input buffer not available");
        }


//        mBufferInfo.flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
        int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
        //FORMAT_CHANGEED < 0 所以需要一个 ||
        while (outputBufferIndex >= 0 || outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                MediaFormat newFormat = mMediaCodec.getOutputFormat();
                MediaMuxerThread mediaMuxerRunnable = this.mediaMuxer.get();
                if (mediaMuxerRunnable != null) {
                    mediaMuxerRunnable.addTrackIndex(MediaMuxerThread.TRACK_VIDEO, newFormat);
                }
            } else {
                ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                    mBufferInfo.size = 0;
                }

                boolean keyFrame = (mBufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;
                Log.i("子线程关键帧", "is key frame :%s"+keyFrame);

                if (mBufferInfo.size != 0) {
                    MediaMuxerThread mediaMuxer = this.mediaMuxer.get();
                    if (mediaMuxer != null && mediaMuxer.isMuxerStart()) {
                        mediaMuxer.addMuxerData(new MediaMuxerThread.MuxerData(MediaMuxerThread.TRACK_VIDEO, outputBuffer, mBufferInfo));
                    }
                }
                mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
            }
            outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
        }
    }

    /**
     * 停止视频编码
     */
    private void stopMediaCodec() {
        if (mMediaCodec != null) {
            mMediaCodec.stop();
            mMediaCodec.release();
            mMediaCodec = null;
        }
        isStart = false;
        Log.e("=====视频录制", "stop video 录制...");
    }

    // 解除所有占用内存的地方
    public void release(){
        this.exit();
        this.stopMediaCodec();

        try{
            scheduler.shutdown();
            scheduler = null;
        }catch (Exception e){}

        mFrameData = null;
        nv21 = null;
        argb = null;
        output = null;

        if(frameBytes != null){
            frameBytes.clear();
            frameBytes = null;
        }
    }
}

使用newFixedThreadPool创建了一个4个线程的线程池,用于加速帧数据编码和水印处理,单一线程效率过低,可能会导致最终视频的帧率过低(因为处理不过来)。

同时采用EncoderPool对象池来减少内存占用。

附上 插件下载地址:点击前往下载地址

ext.dcloud.net.cn/plugin?id=1...

相关推荐
lkbhua莱克瓦242 小时前
Java基础——集合进阶3
java·开发语言·笔记
顺凡2 小时前
删一个却少俩:Antd Tag 多节点同时消失的原因
前端·javascript·面试
蓝-萧2 小时前
使用Docker构建Node.js应用的详细指南
java·后端
多喝开水少熬夜2 小时前
Trie树相关算法题java实现
java·开发语言·算法
前端大卫2 小时前
动态监听DOM元素高度变化
前端·javascript
Cxiaomu2 小时前
React Native App 图表绘制完整实现指南
javascript·react native·react.js
lkbhua莱克瓦243 小时前
Java基础——集合进阶用到的数据结构知识点1
java·数据结构·笔记·github
qq. 28040339843 小时前
vue介绍
前端·javascript·vue.js
Mr.Jessy3 小时前
Web APIs 学习第五天:日期对象与DOM节点
开发语言·前端·javascript·学习·html
速易达网络3 小时前
HTML<output>标签
javascript·css·css3