Android Opengles mediaCodec 预览录制视频 添加水印详解手机开发
本篇是在Camera 采集数据通过 GLSurfaceView 预览 (二)的基础上修改增加的,上一篇只是通过GLSurfaceView进行预览Camera数据,这篇将会接着学习利用OpenGLES更多的特性和MediaCodec硬编码。
这篇主要记录自己遇到的问题和解决思路,文章底部附带源代码。
1. 将GL_TEXTURE_EXTERNAL_OES纹理转化为GL_TEXTURE_2D纹理因为后续我们需要增加水印、录制等功能,需要使用到的是sampler2D纹理,而Camera采集的数据外部纹理。
所以第一步就是将外部纹理转为sampler2D纹理,可以借助FBO来实现;
FBO(Frame Buffer Object ) 帧缓冲对象,之前我们都是渲染到窗口系统提供的默认的帧缓冲中,而FBO支持创建一个帧缓冲区,这样就可以不直接渲染到屏幕上,而是渲染到定制的帧缓冲区。FBO支持将一个纹理绑定到 FBO 上,接着后续所有的渲染操作会被存储到纹理图像上。
整体的流程
(1).在CameraSurfaceRender负责创建GL_TEXTURE_EXTERNAL_OES纹理,接受Camera原始数据
mCameraTextureId = GlesUtil.createCameraTexture();
(2). 在OriginalRenderDrawer创建GL_TEXTURE_2D的纹理
mOutputTextureId = GlesUtil.createFrameTexture(width, height);
(3). 在当前的EGL环境下,创建一个FBO
在本章的源码中,RenderDrawerGroups接收负责管理所有的RenderDrawer,包括创建FBO、控制绘制顺序、是否需要绘制到FBO中等
mFrameBuffer = GlesUtil.createFrameBuffer(); public static int createFrameBuffer() { int[] buffers = new int[1]; GLES30.glGenFramebuffers(1, buffers, 0); checkError(); return buffers[0];
(4). 绑定DisplayRenderDrawer的GL_TEXTURE_2D纹理到FBO上,后续的绘制动作就会存储FBO上
public void bindFrameBuffer(int textureId) { GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, mFrameBuffer); GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, textureId, 0);
(5). 执行OriginalRenderDrawer渲染,通过FBO就自然就渲染到了DisplayRenderDrawer的纹理图像上
RenderDrawerGroups
// timestamp 时间戳 transformMatrix 转换矩阵 public void draw(long timestamp, float[] transformMatrix) { // 将绑定到FBO中,最后转换成mOriginalDrawer中的Sample2D纹理 drawRender(mOriginalDrawer, true, timestamp, transformMatrix); ... // 不绑定FBO,直接绘制到屏幕上 drawRender(mDisplayDrawer, false, timestamp, transformMatrix);
RenderDrawerGroups控制渲染流程
public void drawRender(BaseRenderDrawer drawer, boolean useFrameBuffer, long timestamp, float[] transformMatrix) { if (useFrameBuffer) { // 绑定到FBO中 bindFrameBuffer(drawer.getOutputTextureId()); drawer.draw(timestamp, transformMatrix); if (useFrameBuffer) { unBindFrameBuffer();
(6). 解除FBO绑定,恢复默认的帧缓冲区
public void unBindFrameBuffer() { GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, 0);2. 叠加水印纹理
如果上面转换完成后,那么水印的叠加并不难,同样使用到了FBO,在纹理基础叠加一层水印的绘制混合
WaterMarkRenderDrawer负责渲染绘制水印图片
(1). 创建水印图片纹理
mMarkTextureId = GlesUtil.loadBitmapTexture(mBitmap);
(2). 渲染绘制水印图片
使用了Blend 颜色混合,具体参考轻松搞定 Blend 颜色混合
public void draw(long timestamp, float[] transformMatrix) { useProgram(); //clear(); viewPort(40, 75, mBitmap.getWidth() * 2, mBitmap.getHeight() * 2); GLES30.glDisable(GLES30.GL_DEPTH_TEST); GLES30.glEnable(GLES30.GL_BLEND); GLES30.glBlendFunc(GLES30.GL_SRC_COLOR, GLES30.GL_DST_ALPHA); onDraw(); GLES30.glDisable(GLES30.GL_BLEND);
(3). 控制渲染顺序
这部分在代码中是通过RenderDrawerGroups控制
public void draw(long timestamp, float[] transformMatrix) { if (mInputTexture == 0 || mFrameBuffer == 0) { Log.e(TAG, "draw: mInputTexture or mFramebuffer or list is zero"); return; drawRender(mOriginalDrawer, true, timestamp, transformMatrix); // 绘制顺序会控制着 水印绘制哪一层 //drawRender(mWaterMarkDrawer, true, timestamp, transformMatrix); drawRender(mDisplayDrawer, false, timestamp, transformMatrix); drawRender(mWaterMarkDrawer, true, timestamp, transformMatrix); ....
绘制顺序会控制着 水印绘制哪一层,可以选择绘制到预览或者仅会知道录制层中;
同理,可以通过这种方法增加各种叠加图片、滤镜等等;
(滤镜也可以直接操作顶点、片元着色器的源码实现)
(1). 创建MediaCodec和MediaMutex
MediaCodec 负责接收绘制的数据,并转换成 h264 数据 MediaMutex 负责混合转换成 mp4 文件public VideoEncoder(int width, int height, File outputFile) throws IOException { int bitRate = height * width * 3 * 8 * FRAME_RATE / 256; mBufferInfo = new MediaCodec.BufferInfo(); MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL); Log.d(TAG, "format: " + format); mEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); mInputSurface = mEncoder.createInputSurface(); mEncoder.start(); mMuxer = new MediaMuxer(outputFile.toString(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); mTrackIndex = -1; mMuxerStarted = false;
(2). 创建MediaCodec的输入Surface
MediaCodec 解码视频支持直接解码到 Surface 上,编码也可以直接从 Surface 采集数据。
之前尝试过从GLSurfaceView的 getHolder().getSurface() 获取到Surface,注入给MeidaCodec进行硬编码,最后会产生错误,提示not persistent surface。
MediaCodec可以通过调用createInputSurface() 方法为输入数据创建一个目标 Surface,我们将数据直接绘制到 MediaCodec 创建的 Surface 可以实现录制功能,这样可以使用GLSurfaceView 的surface 和 MediaCodec 的 surface绘制的东西不一样,如仅在录制的视频中增加水印。
创建一个EGL环境,将纹理绘制到MediaCodec的Surface中:
mInputSurface = mEncoder.createInputSurface();
public void drainEncoder(boolean endOfStream) { if (endOfStream) { mEncoder.signalEndOfInputStream(); while (true) { int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC); if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { if (!endOfStream) { break; } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { if (mMuxerStarted) { throw new RuntimeException("format changed twice"); MediaFormat newFormat = mEncoder.getOutputFormat(); mTrackIndex = mMuxer.addTrack(newFormat); mMuxer.start(); mMuxerStarted = true; } else if (encoderStatus 0) { Log.d(TAG, "unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus); } else { ByteBuffer encodedData = mEncoder.getOutputBuffer(encoderStatus); if (encodedData == null) { throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null"); if ((mBufferInfo.flags MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { mBufferInfo.size = 0; if (mBufferInfo.size != 0) { if (!mMuxerStarted) { throw new RuntimeException("muxer hasnt started"); encodedData.position(mBufferInfo.offset); encodedData.limit(mBufferInfo.offset + mBufferInfo.size); mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo); Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer, ts=" + mBufferInfo.presentationTimeUs); } else { Log.d(TAG, "drainEncoder mBufferInfo: " + mBufferInfo.size); mEncoder.releaseOutputBuffer(encoderStatus, false); if ((mBufferInfo.flags MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { break;
(3). 利用共享GLSurfaceview 的 EGLContext 创建EGL环境
注意:在GLSurfaceView的GLThread中获取EGLContext
@Override public void create() { mEglContext = EGL14.eglGetCurrentContext();
创建我们自己的GLThread:
@Override public void run() { Looper.prepare(); mMsgHandler = new MsgHandler(); Looper.loop();
private class MsgHandler extends Handler { public static final int MSG_START_RECORD = 1; public static final int MSG_STOP_RECORD = 2; public static final int MSG_UPDATE_CONTEXT = 3; public static final int MSG_UPDATE_SIZE = 4; public static final int MSG_FRAME = 5; public static final int MSG_QUIT = 6; public MsgHandler() {
case MSG_START_RECORD: prepareVideoEncoder((EGLContext) msg.obj, msg.arg1, msg.arg2); break; case MSG_STOP_RECORD: stopVideoEncoder(); break; case MSG_UPDATE_CONTEXT: updateEglContext((EGLContext) msg.obj); break; case MSG_UPDATE_SIZE: updateChangedSize(msg.arg1, msg.arg2); break; case MSG_FRAME: drawFrame((long)msg.obj); break; case MSG_QUIT: quitLooper(); break; default: break;
private void prepareVideoEncoder(EGLContext context, int width, int height) { try { mEglHelper = new EGLHelper(); mEglHelper.createGL(context); mVideoPath = StorageUtil.getVedioPath(true) + "glvideo.mp4"; mVideoEncoder = new VideoEncoder(width, height, new File(mVideoPath)); mEglSurface = mEglHelper.createWindowSurface(mVideoEncoder.getInputSurface()); boolean error = mEglHelper.makeCurrent(mEglSurface); if (!error) { Log.e(TAG, "prepareVideoEncoder: make current error"); onCreated(); } catch (IOException e) { e.printStackTrace();
private void drawFrame(long timeStamp) { Log.d(TAG, "drawFrame: " + timeStamp ); mEglHelper.makeCurrent(mEglSurface); mVideoEncoder.drainEncoder(false); onDraw(); mEglHelper.setPresentationTime(mEglSurface, timeStamp); mEglHelper.swapBuffers(mEglSurface);
(4). 实现控制绘制
把上述流程封装到RecordRenderDrawer中
RenderDrawerGroups就可以直接将他当作RenderDrawer统一控制
public void draw(long timestamp, float[] transformMatrix) { if (mInputTexture == 0 || mFrameBuffer == 0) { Log.e(TAG, "draw: mInputTexture or mFramebuffer or list is zero"); return; drawRender(mOriginalDrawer, true, timestamp, transformMatrix); // 绘制顺序会控制着 水印绘制哪一层 //drawRender(mWaterMarkDrawer, true, timestamp, transformMatrix); drawRender(mDisplayDrawer, false, timestamp, transformMatrix); drawRender(mWaterMarkDrawer, true, timestamp, transformMatrix); // mRecordDrawer是绘制到MediaCodec中进行录制 drawRender(mRecordDrawer, false, timestamp, transformMatrix);完成的效果 源码地址
云图网
参考云图网
6273.html
app程序应用开发手机开发无线开发移动端开发相关文章
- Android修改字体_android设置字体样式
- 【错误记录】未安装该应用 ( 在 Android 12 之后 组件设置 android:exported=“false“ 属性 )
- Android 两种方式实现类似水波扩散效果详解手机开发
- Android MVP详解手机开发
- 社区(OkCommunity)Android 通讯app开发简介详解手机开发
- Android版本检测更新详解手机开发
- android 通话录音功能详解手机开发
- Android开发学习总结——Android开发的一些相关概念详解手机开发
- [android] 练习使用ListView(二)详解手机开发
- [android] 手机卫士手机实现短信指令获取位置详解手机开发
- [android] 手机卫士自定义对话框布局详解手机开发
- [android] 服务的生命周期(混合方式)详解手机开发
- 关于Android的UI测试详解手机开发
- Android Topeka介绍详解手机开发
- android的Drawable详解手机开发
- android eclipse写layout文件失效问题解决详解手机开发
- 修改android最小堆内存详解手机开发
- Android ormLite复杂条件查询详解手机开发
- android JNI学习详解手机开发
- android EventBus详解(三)手机开发
- android 自定义Lint详解手机开发
- Android View底层到底是怎么绘制的详解手机开发
- 再谈Android动态链接库详解手机开发
- Android开发把项目打包成apk详解手机开发
- Android 在广播接收器中弹出对话框详解手机开发
- 实现android apk反编译后代码混淆详解手机开发
- 安卓编年史(25):Android 4.4,奇巧——更完美,更少的内存占用(2)