最近需要开发一个类似行车记录仪的app,其中需要给录制的视频添加动态水印。我使用的是OpenGL开发的,刚开始实现的是静态水印,后面才实现的动态水印。

先上效果图,左下角的是静态水印,中间偏下的是时间水印(动态水印):

一、静态水印
实现原理:录像时是通过OpenGL把图像渲染到GLSurfaceView上的,通俗的讲,就是把图片画到一块画布上,然后展示出来。添加图片水印,就是把水印图片跟录制的图像一起画到画布上。

这是加载纹理跟阴影的Java类

package com.audiovideo.camera.blog;import android.opengl.GLES20;/*** Created by fenghaitao on 2019/9/12.*/public class WaterSignSProgram{private static int programId;private static final String VERTEX_SHADER ="uniform mat4 uMVPMatrix;\n" +"attribute vec4 aPosition;\n" +"attribute vec4 aTextureCoord;\n" +"varying vec2 vTextureCoord;\n" +"void main() {\n" +"    gl_Position = uMVPMatrix * aPosition;\n" +"    vTextureCoord = aTextureCoord.xy;\n" +"}\n";private static final String FRAGMENT_SHADER ="precision mediump float;\n" +"varying vec2 vTextureCoord;\n" +"uniform sampler2D sTexture;\n" +"void main() {\n" +"    gl_FragColor = texture2D(sTexture, vTextureCoord);\n" +"}\n";public WaterSignSProgram() {programId = loadShader(VERTEX_SHADER, FRAGMENT_SHADER);uMVPMatrixLoc = GLES20.glGetUniformLocation(programId, "uMVPMatrix");checkLocation(uMVPMatrixLoc, "uMVPMatrix");aPositionLoc = GLES20.glGetAttribLocation(programId, "aPosition");checkLocation(aPositionLoc, "aPosition");aTextureCoordLoc = GLES20.glGetAttribLocation(programId, "aTextureCoord");checkLocation(aTextureCoordLoc, "aTextureCoord");sTextureLoc = GLES20.glGetUniformLocation(programId, "sTexture");checkLocation(sTextureLoc, "sTexture");}public int uMVPMatrixLoc;public int aPositionLoc;public int aTextureCoordLoc;public int sTextureLoc;public static void checkLocation(int location, String label) {if (location < 0) {throw new RuntimeException("Unable to locate '" + label + "' in program");}}/*** 加载编译连接阴影* @param vss source of vertex shader* @param fss source of fragment shader* @return*/
public static int loadShader(final String vss, final String fss) {Log.v(TAG, "loadShader:");int vs = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);GLES20.glShaderSource(vs, vss);GLES20.glCompileShader(vs);final int[] compiled = new int[1];GLES20.glGetShaderiv(vs, GLES20.GL_COMPILE_STATUS, compiled, 0);if (compiled[0] == 0) {Log.e(TAG, "Failed to compile vertex shader:"+ GLES20.glGetShaderInfoLog(vs));GLES20.glDeleteShader(vs);vs = 0;}int fs = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);GLES20.glShaderSource(fs, fss);GLES20.glCompileShader(fs);GLES20.glGetShaderiv(fs, GLES20.GL_COMPILE_STATUS, compiled, 0);if (compiled[0] == 0) {Log.w(TAG, "Failed to compile fragment shader:"+ GLES20.glGetShaderInfoLog(fs));GLES20.glDeleteShader(fs);fs = 0;}final int program = GLES20.glCreateProgram();GLES20.glAttachShader(program, vs);GLES20.glAttachShader(program, fs);GLES20.glLinkProgram(program);return program;
}/*** terminatinng, this should be called in GL context*/public static void release() {if (programId >= 0)GLES20.glDeleteProgram(programId);programId = -1;}
}
package com.audiovideo.camera.blog;import android.opengl.GLES20;
import android.opengl.Matrix;import com.audiovideo.camera.glutils.GLDrawer2D;
import com.audiovideo.camera.utils.LogUtil;import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;这是画水印的Java类
/*** Created by fenghaitao on 2019/9/12.*/public class WaterSignature {private static final String VERTEX_SHADER ="uniform mat4 uMVPMatrix;\n" +"attribute vec4 aPosition;\n" +"attribute vec4 aTextureCoord;\n" +"varying vec2 vTextureCoord;\n" +"void main() {\n" +"    gl_Position = uMVPMatrix * aPosition;\n" +"    vTextureCoord = aTextureCoord.xy;\n" +"}\n";private static final String FRAGMENT_SHADER ="precision mediump float;\n" +"varying vec2 vTextureCoord;\n" +"uniform sampler2D sTexture;\n" +"void main() {\n" +"    gl_FragColor = texture2D(sTexture, vTextureCoord);\n" +"}\n";public static final int SIZE_OF_FLOAT = 4;/*** 一个“完整”的正方形,从两维延伸到-1到1。* 当 模型/视图/投影矩阵是都为单位矩阵的时候,这将完全覆盖视口。* 纹理坐标相对于矩形是y反的。* (This seems to work out right with external textures from SurfaceTexture.)*/private static final float FULL_RECTANGLE_COORDS[] = {-1.0f, -1.0f,   // 0 bottom left1.0f, -1.0f,   // 1 bottom right-1.0f,  1.0f,   // 2 top left1.0f,  1.0f,   // 3 top right};private static final float FULL_RECTANGLE_TEX_COORDS[] = {0.0f, 1.0f,     //0 bottom left     //0.0f, 0.0f, // 0 bottom left1.0f, 1.0f,     //1 bottom right    //1.0f, 0.0f, // 1 bottom right0.0f, 0.0f,     //2 top left        //0.0f, 1.0f, // 2 top left1.0f, 0.0f,     //3 top right       //1.0f, 1.0f, // 3 top right};private FloatBuffer mVertexArray;private FloatBuffer mTexCoordArray;private int mCoordsPerVertex;private int mCoordsPerTexture;private int mVertexCount;private int mVertexStride;private int mTexCoordStride;private int hProgram;public float[] mProjectionMatrix = new float[16];// 投影矩阵public float[] mViewMatrix = new float[16]; // 摄像机位置朝向9参数矩阵public float[] mModelMatrix = new float[16];// 模型变换矩阵public float[] mMVPMatrix = new float[16];// 获取具体物体的总变换矩阵private float[] getFinalMatrix() {Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mModelMatrix, 0);Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mMVPMatrix, 0);return mMVPMatrix;}public WaterSignature() {mVertexArray = createFloatBuffer(FULL_RECTANGLE_COORDS);mTexCoordArray = createFloatBuffer(FULL_RECTANGLE_TEX_COORDS);mCoordsPerVertex = 2;mCoordsPerTexture = 2;mVertexCount = FULL_RECTANGLE_COORDS.length / mCoordsPerVertex; // 4mTexCoordStride = 2 * SIZE_OF_FLOAT;mVertexStride = 2 * SIZE_OF_FLOAT;Matrix.setIdentityM(mProjectionMatrix, 0);Matrix.setIdentityM(mViewMatrix, 0);Matrix.setIdentityM(mModelMatrix, 0);Matrix.setIdentityM(mMVPMatrix, 0);hProgram = GLDrawer2D.loadShader(VERTEX_SHADER, FRAGMENT_SHADER);GLES20.glUseProgram(hProgram);}private FloatBuffer createFloatBuffer(float[] coords) {ByteBuffer bb = ByteBuffer.allocateDirect(coords.length * SIZE_OF_FLOAT);bb.order(ByteOrder.nativeOrder());FloatBuffer fb = bb.asFloatBuffer();fb.put(coords);fb.position(0);return fb;}private WaterSignSProgram mProgram;public void setShaderProgram(WaterSignSProgram mProgram) {this.mProgram = mProgram;}public void drawFrame(int mTextureId) {GLES20.glUseProgram(hProgram);// 设置纹理GLES20.glActiveTexture(GLES20.GL_TEXTURE0);GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId);GLES20.glUniform1i(mProgram.sTextureLoc, 0);GlUtil.checkGlError("GL_TEXTURE_2D sTexture");// 设置 model / view / projection 矩阵GLES20.glUniformMatrix4fv(mProgram.uMVPMatrixLoc, 1, false, getFinalMatrix(), 0);GlUtil.checkGlError("glUniformMatrix4fv uMVPMatrixLoc");// 使用简单的VAO 设置顶点坐标数据GLES20.glEnableVertexAttribArray(mProgram.aPositionLoc);GLES20.glVertexAttribPointer(mProgram.aPositionLoc, mCoordsPerVertex,GLES20.GL_FLOAT, false, mVertexStride, mVertexArray);GlUtil.checkGlError("VAO aPositionLoc");// 使用简单的VAO 设置纹理坐标数据GLES20.glEnableVertexAttribArray(mProgram.aTextureCoordLoc);GLES20.glVertexAttribPointer(mProgram.aTextureCoordLoc, mCoordsPerTexture,GLES20.GL_FLOAT, false, mTexCoordStride, mTexCoordArray);GlUtil.checkGlError("VAO aTextureCoordLoc");// GL_TRIANGLE_STRIP三角形带,这就为啥只需要指出4个坐标点,就能画出两个三角形了。GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, mVertexCount);// Done -- 解绑~GLES20.glDisableVertexAttribArray(mProgram.aPositionLoc);GLES20.glDisableVertexAttribArray(mProgram.aTextureCoordLoc);GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);GLES20.glUseProgram(0);}/*** terminatinng, this should be called in GL context*/public void release() {if (hProgram >= 0)GLES20.glDeleteProgram(hProgram);hProgram = -1;}/*** 删除texture*/public static void deleteTex(final int hTex) {LogUtil.v("WaterSignature", "deleteTex:");final int[] tex = new int[] {hTex};GLES20.glDeleteTextures(1, tex, 0);}}

没时间了。先写到这,后面是调用,迟点再写。

下面是如何把水印绘制到画布上:
1、在SurfaceTexture的onSurfaceCreated方法中初始化并设置阴影;

      @Overridepublic void onSurfaceCreated(final GL10 unused, final EGLConfig config) {LogUtil.v(TAG, "onSurfaceCreated:");// This renderer required OES_EGL_image_external extensionfinal String extensions = GLES20.glGetString(GLES20.GL_EXTENSIONS);    // API >= 8// 使用黄色清除界面GLES20.glClearColor(1.0f, 1.0f, 0.0f, 1.0f);//设置水印if (mWaterSign == null) {mWaterSign = new WaterSignature();}//设置阴影mWaterSign.setShaderProgram(new WaterSignSProgram());mSignTexId = loadTexture(MyApplication.getContext(), R.mipmap.watermark);}

这里是生成mSignTexId 的方法,把该图像与纹理id绑定并返回:

public static int loadTexture(Context context, int resourceId) {final int[] textureObjectIds = new int[1];GLES20.glGenTextures(1, textureObjectIds, 0);if(textureObjectIds[0] == 0){Log.e(TAG,"Could not generate a new OpenGL texture object!");return 0;}final BitmapFactory.Options options = new BitmapFactory.Options();options.inScaled = false;   //指定需要的是原始数据,非压缩数据final Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resourceId, options);if(bitmap == null){Log.e(TAG, "Resource ID "+resourceId + "could not be decode");GLES20.glDeleteTextures(1, textureObjectIds, 0);return 0;}//告诉OpenGL后面纹理调用应该是应用于哪个纹理对象GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureObjectIds[0]);//设置缩小的时候(GL_TEXTURE_MIN_FILTER)使用mipmap三线程过滤GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR);//设置放大的时候(GL_TEXTURE_MAG_FILTER)使用双线程过滤GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);//Android设备y坐标是反向的,正常图显示到设备上是水平颠倒的,解决方案就是设置纹理包装,纹理T坐标(y)设置镜面重复//ball读取纹理的时候  t范围坐标取正常值+1//GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_MIRRORED_REPEAT);GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);bitmap.recycle();//快速生成mipmap贴图GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);//解除纹理操作的绑定GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);return textureObjectIds[0];
}

2、在绘制方法onDrawFrame中绘制画面的同时把水印绘制进去;

/*** 绘图到glsurface* 我们将rendermode设置为glsurfaceview.rendermode_when_dirty,* 仅当调用requestrender时调用此方法(=需要更新纹理时)* 如果不在脏时设置rendermode,则此方法的最大调用速度为60fps。*/@Overridepublic void onDrawFrame(final GL10 unused) {GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);GLES20.glEnable(GLES20.GL_BLEND);//开启GL的混合模式,即图像叠加GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);/***中间这里是你绘制的预览画面*///画水印(非动态)GLES20.glViewport(20, 20, 288, 120);mWaterSign.drawFrame(mSignTexId);}

这里最重要的是要开启GL的混合模式,即图像叠加,不然你绘制的水印会覆盖原先的预览画面

//开启GL的混合模式,即图像叠加
GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);

二、动态水印(时间水印)
原理:把时间字符单个转化为图片,然后再把它一个个的绘制在画布上。如2020-02-14 10:24:30这种时间格式总共包含0123456789 - :这12个字符,全部先转成图片,再去匹配绘制到画布。
下面是一点的相关的代码:
注意:时间格式必须为 yyyy-MM-dd HH:mm:ss ,因为下面是截取文字,格式不对的话会抛异常。

/*** 添加时间水印* 原理:将文字转成图片,使用OpenGL将图片画到视频上* 实现方法:将时间水印所需的文字转化为图片(避免每次实时转换导致大量内存被消耗),如:2019-09-05 16:52:18  共16张图片。* 每张图片绑定一个纹理;获取当前时间切出单个字符,使用OpenGL将对应的字符纹理画上去。* @param time* @param x* @param y    位于屏幕的(x,y)坐标点*/
private void drawWaterSign(String time, int x, int y) {if ("".equals(time)) {return;}//画水印GLES20.glViewport(x, y, 220, 60);mWaterSign.drawFrame(mWaterTexId[Integer.parseInt(time.substring(0, 1))]);GLES20.glViewport(x + 15, y, 220, 60);mWaterSign.drawFrame(mWaterTexId[Integer.parseInt(time.substring(1, 2))]);GLES20.glViewport(x + 15 * 2, y, 220, 60);mWaterSign.drawFrame(mWaterTexId[Integer.parseInt(time.substring(2, 3))]);GLES20.glViewport(x + 15 * 3, y, 220, 60);mWaterSign.drawFrame(mWaterTexId[Integer.parseInt(time.substring(3, 4))]);GLES20.glViewport(x + 15 * 4, y, 220, 60);mWaterSign.drawFrame(mWaterTexId[10]); // -GLES20.glViewport(x + 15 * 5, y, 220, 60);mWaterSign.drawFrame(mWaterTexId[Integer.parseInt(time.substring(5, 6))]);GLES20.glViewport(x + 15 * 6, y, 220, 60);mWaterSign.drawFrame(mWaterTexId[Integer.parseInt(time.substring(6, 7))]);GLES20.glViewport(x + 15 * 7, y, 220, 60);mWaterSign.drawFrame(mWaterTexId[10]); // -GLES20.glViewport(x + 15 * 8, y, 220, 60);mWaterSign.drawFrame(mWaterTexId[Integer.parseInt(time.substring(8, 9))]);GLES20.glViewport(x + 15 * 9, y, 220, 60);mWaterSign.drawFrame(mWaterTexId[Integer.parseInt(time.substring(9, 10))]);GLES20.glViewport(x + 15 * 11, y, 220, 60);mWaterSign.drawFrame(mWaterTexId[Integer.parseInt(time.substring(11, 12))]);GLES20.glViewport(x + 15 * 12, y, 220, 60);mWaterSign.drawFrame(mWaterTexId[Integer.parseInt(time.substring(12, 13))]);GLES20.glViewport(x + 15 * 13, y, 220, 60);mWaterSign.drawFrame(mWaterTexId[11]); // :GLES20.glViewport(x + 15 * 14, y, 220, 60);mWaterSign.drawFrame(mWaterTexId[Integer.parseInt(time.substring(14, 15))]);GLES20.glViewport(x + 15 * 15, y, 220, 60);mWaterSign.drawFrame(mWaterTexId[Integer.parseInt(time.substring(15, 16))]);GLES20.glViewport(x + 15 * 16, y, 220, 60);mWaterSign.drawFrame(mWaterTexId[11]); // :GLES20.glViewport(x + 15 * 17, y, 220, 60);mWaterSign.drawFrame(mWaterTexId[Integer.parseInt(time.substring(17, 18))]);GLES20.glViewport(x + 15 * 18, y, 220, 60);mWaterSign.drawFrame(mWaterTexId[Integer.parseInt(time.substring(18, 19))]);
}

调用方法:

最近把添加水印的代码抽出来做成demo,需要的可以私信,这边有偿提供demo。

Android使用Opengl录像时添加(动态)水印相关推荐

  1. Android录像时添加时间水印

    在网上搜索整理了下,有三篇有用的文章,原理也是一样. 实现步骤说明在这里Android 录制视频添加时间水印 上面也仅给出了步骤,具体代码在增加录像时间戳水印. camera框架介绍 可以找到.从博客 ...

  2. 【视频加水印】Video Watermark Pro视频添加动态水印(附工具下载地址)

    <Video Watermark Pro>是一款专门给视频加水印的软件,这个软件可以帮用户快速加水印,而且可以批量加水印,软件操作起来简单又快捷,有需要的用户不要错过. 软件特色 1.将水 ...

  3. ffmpeg录制视频时添加时间水印

    IPcamera录制视频,经常用到时间戳水印.ffmpeg avfilter模块,可以实现水印的添加 设置filter const char *filters_descr = "drawte ...

  4. Jsp 页面添加动态水印

    //从session 中读取需要的信息 <% javacommon.base.LoginUser user = (javacommon.base.LoginUser) session.getAt ...

  5. Android 实现openGL录像添加静态图片水印

    /**  * 贴纸滤镜  */ public class WaterFilter extends AbstractFrameFilter {     private static final Stri ...

  6. Unity Android平台读取文件时添加了权限依然报错“Access to the path is denied“

    Unity 调用Android读取文件"Access to the path is denied" 添加权限依然报错 记录一下Unity 调用Android读取文件"Ac ...

  7. Android系统Camera录像过程分析

      最近调试系统Camera,遇到如下问题:在录像过程中,拔掉Camera:会出现应用程序卡死现象. 先说说之前的设计架构: 当用户拔掉Camera时,会给应用程序发送广播:当应用程序收到广播后调用A ...

  8. Android 录像添加时间戳水印

    最近项目中需要后台录像并添加时间戳,就类似监控视频,直接放效果图了, demo界面功能如图:跑的时候注意自己到设置加相机权限 这个demo主要做到了两点,一.添加时间戳水印.二.暂停,继续录像.git ...

  9. android利用EpMedia给录像添加时间水印

    做出来的例子效果如下: 第一步:集成EpMedia, 步骤在大神的github上都有,地址如下: https://github.com/yangjie10930/EpMedia 添加时间水印,我的方法 ...

最新文章

  1. Spring Boot 五种热部署方式,极速开发就是生产力!
  2. Leangoo团队敏捷开发实现过程
  3. minimum在java中的意思_Java Calendar getMinimum()用法及代码示例
  4. 零基础带你学习MySQL—多子句查询(十九)
  5. 光斑质心检测之曲线拟合求亚像素位置的三种方式
  6. 【转】js中forEach回调同异步问题
  7. 学子商城代码2(项目 第十六阶段)
  8. 联想yoga13装win7步骤介绍
  9. Torrent File Editor——好软推荐
  10. Switch视频转换器方案
  11. 11408考研复习规划
  12. BZOJ 2037 [Sdoi2008] Sue的小球
  13. 国美易卡管理云平台,国美易卡采用大量另类数据
  14. 《14天从0到1学Java》第一天之04第一行Java代码
  15. Elastic:集群相关知识点总结(一)数据流 Data Stream、索引生命周期 ILM、可搜索快照 searchable snapshots、跨集群搜索 CCS、跨集群复制 CCR
  16. 【php学习之路】微信公众帐号
  17. MATLAB求复杂函数积分
  18. ftp常见报错之Use PORT or PASV first解决思路和方法
  19. 内外网通过公网IP访问DMZ主机
  20. VCam 能做什么?

热门文章

  1. 给我5分钟,手把手带你学会定时任务!
  2. HTML中的幽灵节点
  3. ps绘制超级五角星教程
  4. wine下微信中文乱码修订
  5. python从TXT导入两列数据绘图 直线多点等分坐标可视化
  6. 关于笔记本电脑修改显示器刷新频率后,重新开机时会自动修改为上一次的频率问题
  7. out of synch
  8. 让dede织梦显示最新文章前面加小图标
  9. 【c#】DGV中复制粘贴数据最详细代码
  10. Unity 讯飞实时语音转写(二)—— 接收转写结果