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...

相关推荐
m0_5719575830 分钟前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
悦涵仙子34 分钟前
CSS中的变量应用——:root,Sass变量,JavaScript中使用Sass变量
javascript·css·sass
兔老大的胡萝卜35 分钟前
ppk谈JavaScript,悟透JavaScript,精通CSS高级Web,JavaScript DOM编程艺术,高性能JavaScript pdf
前端·javascript
魔道不误砍柴功3 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2343 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨3 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
cs_dn_Jie4 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
测开小菜鸟4 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
开心工作室_kaic5 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿5 小时前
webWorker基本用法
前端·javascript·vue.js