今天浅谈:openGLopenSLES 代码有点长一丶openGL特效(1)1。MediaCodec MediaCodec是Android4。1。2(API16)提供的一套编解码API。它的使用非常简单,它存在一个输入缓冲区与一个输出缓冲区,在编码时我们将数据塞入输入缓冲区,然后从输出缓冲区取出编码完成后的数据就可以了。 除了直接操作输入缓冲区之外,还有另一种方式来告知MediaCodec需要编码的数据,那就是: 使用此接口创建一个Surface,然后我们在这个Surface中作画,MediaCodec就能够自动的编码Surface中的画作,我们只需要从输出缓冲区取出编码完成之后的数据即可。 此前,我们使用OpenGL进行绘画显示在屏幕上,然而想要复制屏幕图像到cpu内存中却不是一件非常轻松的事情。所以我们可以直接将OpenGL显示到屏幕中的图像,同时绘制到MediaCodeccreateInputSurface当中去。 PBO(PixelBufferObject,像素缓冲对象)通过直接的内存访问(DirectMemoryAccess,DMA)高速的复制屏幕图像像素数据到CPU内存,但这里我们直接使用createInputSurface更简单 录制我们在另外一个线程中进行(录制现场),所以录制的EGL环境和显示的EGL环境(GLSurfaceView,显示线程)是两个独立的工作环境,他们又能够共享上下文资源:显示线程中使用的texture等,需要能够在录制线程中操作(通过录制线程中使用OpenGL绘制到MediaCodec的Surface)。 在这个线程中我们需要自己来:1、配置录制使用的EGL环境(参照GLSurfaceView是怎么配置的) 2、完成将显示的图像绘制到MediaCodec的Surface中 3、编码(H。264)与复用(封装mp4)的工作 【更多音视频学习资料,点击下方链接免费领取,先码住不迷路】 音视频开发基础知识和资料包 2。极快、极慢模式视频录制2。1。创摄像头预览 NDK51OpenGL:FBO 定义一个DouyinView继承GLSurfaceView,并setRenderer(douyinRenderer);DouyinRenderer负责渲染DouyinRenderer中创建画布,设置效果CameraFilter写入fbo(帧缓存),ScreenFilter负责往屏幕上渲染 AbstractFilterpublicabstractclassAbstractFilter{ protectedFloatBuffermGLVertexB protectedFloatBuffermGLTextureB 顶点着色 protectedintmVertexShaderId; 片段着色 protectedintmFragmentShaderId; protectedintmGLProgramId; 顶点着色器 attributevec4 赋值给glPosition(顶点) protectedintvP varyingvec2textureC protectedintvC uniformmat4vM protectedintvM 片元着色器 Samlpe2D扩展samplerExternalOES protectedintvT protectedintmOutputW protectedintmOutputH publicAbstractFilter(Contextcontext,intvertexShaderId,intfragmentShaderId){ this。mVertexShaderIdvertexShaderId; this。mFragmentShaderIdfragmentShaderId; 4个点x,y42float4字节所以424 mGLVertexBufferByteBuffer。allocateDirect(424) 。order(ByteOrder。nativeOrder()) 。asFloatBuffer(); mGLVertexBuffer。clear(); float〔〕VERTEX{ 1。0f,1。0f, 1。0f,1。0f, 1。0f,1。0f, 1。0f,1。0f }; mGLVertexBuffer。put(VERTEX); mGLTextureBufferByteBuffer。allocateDirect(424) 。order(ByteOrder。nativeOrder()) 。asFloatBuffer(); mGLTextureBuffer。clear(); float〔〕TEXTURE{ 0。0f,1。0f, 1。0f,1。0f, 0。0f,0。0f, 1。0f,0。0f }; mGLTextureBuffer。put(TEXTURE); initilize(context); initCoordinate(); } protectedvoidinitilize(Contextcontext){ StringvertexSharderOpenGLUtils。readRawTextFile(context,mVertexShaderId); StringframentShaderOpenGLUtils。readRawTextFile(context,mFragmentShaderId); mGLProgramIdOpenGLUtils。loadProgram(vertexSharder,framentShader); 获得着色器中的attribute变量position的索引值 vPositionGLES20。glGetAttribLocation(mGLProgramId,vPosition); vCoordGLES20。glGetAttribLocation(mGLProgramId,vCoord); vMatrixGLES20。glGetUniformLocation(mGLProgramId,vMatrix); 获得Uniform变量的索引值 vTextureGLES20。glGetUniformLocation(mGLProgramId,vTexture); } publicvoidonReady(intwidth,intheight){ mOutputW mOutputH } publicvoidrelease(){ GLES20。glDeleteProgram(mGLProgramId); } publicintonDrawFrame(inttextureId){ 设置显示窗口 GLES20。glViewport(0,0,mOutputWidth,mOutputHeight); 使用着色器 GLES20。glUseProgram(mGLProgramId); 传递坐标 mGLVertexBuffer。position(0); GLES20。glVertexAttribPointer(vPosition,2,GLES20。GLFLOAT,false,0,mGLVertexBuffer); GLES20。glEnableVertexAttribArray(vPosition); mGLTextureBuffer。position(0); GLES20。glVertexAttribPointer(vCoord,2,GLES20。GLFLOAT,false,0,mGLTextureBuffer); GLES20。glEnableVertexAttribArray(vCoord); GLES20。glActiveTexture(GLES20。GLTEXTURE0); GLES20。glBindTexture(GLES20。GLTEXTURE2D,textureId); GLES20。glUniform1i(vTexture,0); GLES20。glDrawArrays(GLES20。GLTRIANGLESTRIP,0,4); returntextureId; } 修改坐标 protectedvoidinitCoordinate(){ } } ScreenFilter 负责往屏幕上渲染 publicclassScreenFilterextendsAbstractFilter{ publicScreenFilter(Contextcontext){ super(context,R。raw。basevertex,R。raw。basefrag); } } CameraFilterpublicclassCameraFilterextendsAbstractFilter{ privateint〔〕mFrameB privateint〔〕mFrameBufferT privatefloat〔〕 publicCameraFilter(Contextcontext){ super(context,R。raw。cameravertex2,R。raw。camerafrag2); } Override protectedvoidinitCoordinate(){ mGLTextureBuffer。clear(); 摄像头是颠倒的 float〔〕TEXTURE{ 0。0f,0。0f, 1。0f,0。0f, 0。0f,1。0f, 1。0f,1。0f }; 调整好了镜像 float〔〕TEXTURE{ 1。0f,0。0f, 0。0f,0。0f, 1。0f,1。0f, 0。0f,1。0f, }; 修复旋转逆时针旋转90度 float〔〕TEXTURE{ 0。0f,0。0f, 0。0f,1。0f, 1。0f,0。0f, 1。0f,1。0f }; mGLTextureBuffer。put(TEXTURE); } Override publicvoidrelease(){ super。release(); destroyFrameBuffers(); } publicvoiddestroyFrameBuffers(){ 删除fbo的纹理 if(mFrameBufferTextures!null){ GLES20。glDeleteTextures(1,mFrameBufferTextures,0); mFrameBufferT } 删除fbo if(mFrameBuffers!null){ GLES20。glDeleteFramebuffers(1,mFrameBuffers,0); mFrameB } } Override publicvoidonReady(intwidth,intheight){ super。onReady(width,height); if(mFrameBuffers!null){ destroyFrameBuffers(); } fbo的创建(缓存) 1、创建fbo(离屏屏幕) mFrameBuffersnewint〔1〕; 1、创建几个fbo2、保存fboid的数据3、从这个数组的第几个开始保存 GLES20。glGenFramebuffers(mFrameBuffers。length,mFrameBuffers,0); 2、创建属于fbo的纹理 mFrameBufferTexturesnewint〔1〕;用来记录纹理id 创建纹理 OpenGLUtils。glGenTextures(mFrameBufferTextures); 让fbo与纹理发生关系 创建一个2d的图像 目标2d纹理等级格式宽、高格式数据类型(byte)像素数据 GLES20。glBindTexture(GLES20。GLTEXTURE2D,mFrameBufferTextures〔0〕);GLES20。glTexImage2D(GLES20。GLTEXTURE2D,0,GLES20。GLRGBA,mOutputWidth,mOutputHeight,0,GLES20。GLRGBA,GLES20。GLUNSIGNEDBYTE,null); 让fbo与纹理绑定起来,后续的操作就是在操作fbo与这个纹理上了 GLES20。glBindFramebuffer(GLES20。GLFRAMEBUFFER,mFrameBuffers〔0〕);GLES20。glFramebufferTexture2D(GLES20。GLFRAMEBUFFER,GLES20。GLCOLORATTACHMENT0,GLES20。GLTEXTURE2D,mFrameBufferTextures〔0〕,0); 解绑 GLES20。glBindTexture(GLES20。GLTEXTURE2D,0); GLES20。glBindFramebuffer(GLES20。GLFRAMEBUFFER,0); } Override publicintonDrawFrame(inttextureId){ 设置显示窗口 GLES20。glViewport(0,0,mOutputWidth,mOutputHeight); 不调用的话就是默认的操作glsurfaceview中的纹理了。显示到屏幕上了 这里我们还只是把它画到fbo中(缓存) GLES20。glBindFramebuffer(GLES20。GLFRAMEBUFFER,mFrameBuffers〔0〕); 使用着色器 GLES20。glUseProgram(mGLProgramId); 传递坐标 mGLVertexBuffer。position(0); GLES20。glVertexAttribPointer(vPosition,2,GLES20。GLFLOAT,false,0,mGLVertexBuffer); GLES20。glEnableVertexAttribArray(vPosition); mGLTextureBuffer。position(0); GLES20。glVertexAttribPointer(vCoord,2,GLES20。GLFLOAT,false,0,mGLTextureBuffer); GLES20。glEnableVertexAttribArray(vCoord); 变换矩阵 GLES20。glUniformMatrix4fv(vMatrix,1,false,matrix,0);GLES20。glActiveTexture(GLES20。GLTEXTURE0); 因为这一层是摄像头后的第一层,所以需要使用扩展的GLTEXTUREEXTERNALOES GLES20。glBindTexture(GLES11Ext。GLTEXTUREEXTERNALOES,textureId); GLES20。glUniform1i(vTexture,0); GLES20。glDrawArrays(GLES20。GLTRIANGLESTRIP,0,4); GLES20。glBindTexture(GLES11Ext。GLTEXTUREEXTERNALOES,0); GLES20。glBindFramebuffer(GLES20。GLFRAMEBUFFER,0); 返回fbo的纹理id returnmFrameBufferTextures〔0〕; } publicvoidsetMatrix(float〔〕matrix){ this。 } } 【更多音视频学习资料,点击下方链接免费领取,先码住不迷路】 C程序员必看,抓住音视频开发的大浪潮!冲击年薪60万 2。渲染时定义一个录制类MediaRecorderpublicclassDouyinRendererimplementsGLSurfaceView。Renderer, SurfaceTexture。OnFrameAvailableListener{ privateScreenFiltermScreenF privateDouyinViewmV privateCameraHelpermCameraH privateSurfaceTexturemSurfaceT privatefloat〔〕mtxnewfloat〔16〕; privateint〔〕mT privateCameraFiltermCameraF privateMediaRecordermMediaR publicDouyinRenderer(DouyinViewdouyinView){ mViewdouyinV } Override publicvoidonSurfaceCreated(GL10gl,EGLConfigconfig){ 初始化的操作 mCameraHelpernewCameraHelper(Camera。CameraInfo。CAMERAFACINGBACK); 准备好摄像头绘制的画布 通过opengl创建一个纹理id mTexturesnewint〔1〕; 这里可以不配置(当然配置了也可以) GLES20。glGenTextures(mTextures。length,mTextures,0); mSurfaceTexturenewSurfaceTexture(mTextures〔0〕); mSurfaceTexture。setOnFrameAvailableListener(this); 注意:必须在gl线程操作opengl mCameraFilternewCameraFilter(mView。getContext()); mScreenFilternewScreenFilter(mView。getContext()); 渲染线程的EGL上下文 EGLContexteglContextEGL14。eglGetCurrentContext(); mMediaRecordernewMediaRecorder(mView。getContext(),sdcarda。mp4,CameraHelper。HEIGHT,CameraHelper。WIDTH,eglContext); } 画布发生了改变 paramgl paramwidth paramheight Override publicvoidonSurfaceChanged(GL10gl,intwidth,intheight){ 开启预览 mCameraHelper。startPreview(mSurfaceTexture); mCameraFilter。onReady(width,height); mScreenFilter。onReady(width,height); } 开始画画吧 paramgl Override publicvoidonDrawFrame(GL10gl){ 配置屏幕 清理屏幕:告诉opengl需要把屏幕清理成什么颜色 GLES20。glClearColor(0,0,0,0); 执行上一个:glClearColor配置的屏幕颜色 GLES20。glClear(GLES20。GLCOLORBUFFERBIT); 把摄像头的数据先输出来 更新纹理,然后我们才能够使用opengl从 SurfaceTexure当中获得数据进行渲染 mSurfaceTexture。updateTexImage(); surfaceTexture比较特殊,在opengl当中使用的是特殊的采样器samplerExternalOES(不是sampler2D) 获得变换矩阵 mSurfaceTexture。getTransformMatrix(mtx); mCameraFilter。setMatrix(mtx); 责任链 intidmCameraFilter。onDrawFrame(mTextures〔0〕); 加效果滤镜 id效果1。onDrawFrame(id); id效果2。onDrawFrame(id); 。。。。 加完之后再显示到屏幕中去 mScreenFilter。onDrawFrame(id); 进行录制 mMediaRecorder。encodeFrame(id,mSurfaceTexture。getTimestamp()); } publicvoidonSurfaceDestroyed(){ mCameraHelper。stopPreview(); } publicvoidstartRecord(floatspeed){ try{ mMediaRecorder。start(speed); }catch(IOExceptione){ e。printStackTrace(); } } publicvoidstopRecord(){ mMediaRecorder。stop(); } surfaceTexture有一个有效的新数据的时候回调 paramsurfaceTexture Override publicvoidonFrameAvailable(SurfaceTexturesurfaceTexture){ mView。requestRender(); } }3。录制类MediaRecorder MediaRecorder 录制类 publicclassMediaRecorder{ privatefinalContextmC privatefinalStringmP privatefinalintmW privatefinalintmH privatefinalEGLContextmEglC privateMediaCodecmMediaC privateSurfacemInputS privateMediaMuxermMediaM privateHandlermH privateEGLBasemEglB privatebooleanisS privatefloatmS paramcontext上下文 parampath保存视频的地址 paramwidth视频宽 paramheight视频高 还可以让人家传递帧率fps、码率等参数 publicMediaRecorder(Contextcontext,Stringpath,intwidth,intheight,EGLContexteglContext){ mContextcontext。getApplicationContext(); mP mW mH mEglContexteglC } 开始录制视频 paramspeed publicvoidstart(floatspeed)throwsIOException{ mS 配置MediaCodec编码器 视频格式 类型(avc高级编码h264)编码出的宽、高 MediaFormatmediaFormatMediaFormat。createVideoFormat(MediaFormat。MIMETYPEVIDEOAVC,mWidth,mHeight); 参数配置 1500kbs码率 mediaFormat。setInteger(MediaFormat。KEYBITRATE,1500000); 帧率 mediaFormat。setInteger(MediaFormat。KEYFRAMERATE,20); 关键帧间隔 mediaFormat。setInteger(MediaFormat。KEYIFRAMEINTERVAL,20); 颜色格式(RGBYUV) 从surface当中回去 mediaFormat。setInteger(MediaFormat。KEYCOLORFORMAT,MediaCodecInfo。CodecCapabilities。COLORFormatSurface); 编码器 mMediaCodecMediaCodec。createEncoderByType(MediaFormat。MIMETYPEVIDEOAVC); 将参数配置给编码器 mMediaCodec。configure(mediaFormat,null,null,MediaCodec。CONFIGUREFLAGENCODE); 交给虚拟屏幕通过opengl将预览的纹理绘制到这一个虚拟屏幕中 这样MediaCodec就会自动编码inputSurface中的图像 mInputSurfacemMediaCodec。createInputSurface(); H。264 播放: MP4解复用(解封装)解码绘制 封装器复用器 一个mp4的封装器将h。264通过它写出到文件就可以了 mMediaMuxernewMediaMuxer(mPath,MediaMuxer。OutputFormat。MUXEROUTPUTMPEG4); 配置EGL环境 Handler:线程通信 Handler:子线程通知主线程 Looper。loop(); HandlerThreadhandlerThreadnew HandlerThread(VideoCodec); handlerThread。start(); LooperlooperhandlerThread。getLooper(); 用于其他线程通知子线程 mHandlernewHandler(looper); 子线程:EGL的绑定线程,对我们自己创建的EGL环境的opengl操作都在这个线程当中执行 mHandler。post(newRunnable(){ Override publicvoidrun(){ 创建我们的EGL环境(虚拟设备、EGL上下文等) mEglBasenewEGLBase(mContext,mWidth,mHeight,mInputSurface,mEglContext); 启动编码器 mMediaCodec。start(); isS } }); } 传递纹理进来 相当于调用一次就有一个新的图像需要编码 publicvoidencodeFrame(finalinttextureId,finallongtimestamp){ if(!isStart){ } mHandler。post(newRunnable(){ Override publicvoidrun(){ 把图像画到虚拟屏幕 mEglBase。draw(textureId,timestamp); 从编码器的输出缓冲区获取编码后的数据就ok了 getCodec(false); } }); } 获取编码后的数据 paramendOfStream标记是否结束录制 privatevoidgetCodec(booleanendOfStream){ 不录了,给mediacodec一个标记 if(endOfStream){ mMediaCodec。signalEndOfInputStream(); } 输出缓冲区 MediaCodec。BufferInfobufferInfonewMediaCodec。BufferInfo(); 希望将已经编码完的数据都获取到然后写出到mp4文件 while(true){ 等待10ms intstatusmMediaCodec。dequeueOutputBuffer(bufferInfo,10000); 让我们重试1、需要更多数据2、可能还没编码为完(需要更多时间) if(statusMediaCodec。INFOTRYAGAINLATER){ 如果是停止我继续循环 继续循环就表示不会接收到新的等待编码的图像 相当于保证mediacodec中所有的待编码的数据都编码完成了,不断地重试取出编码器中的编码好的数据 标记不是停止,我们退出,下一轮接收到更多数据再来取输出编码后的数据 if(!endOfStream){ 不写这个会卡太久了,没有必要你还是在继续录制的,还能调用这个方法的! } 否则继续 }elseif(statusMediaCodec。INFOOUTPUTFORMATCHANGED){ 开始编码就会调用一次 MediaFormatoutputFormatmMediaCodec。getOutputFormat(); 配置封装器 增加一路指定格式的媒体流视频 indexmMediaMuxer。addTrack(outputFormat); mMediaMuxer。start(); }elseif(statusMediaCodec。INFOOUTPUTBUFFERSCHANGED){ 忽略 }else{ 成功取出一个有效的输出 ByteBufferoutputBuffermMediaCodec。getOutputBuffer(status); 如果获取的ByteBuffer是配置信息,不需要写出到mp4 if((bufferInfo。flagsMediaCodec。BUFFERFLAGCODECCONFIG)!0){ bufferInfo。size0; } if(bufferInfo。size!0){ bufferInfo。presentationTimeUs(long)(bufferInfo。presentationTimeUsmSpeed); 写到mp4 根据偏移定位 outputBuffer。position(bufferInfo。offset); ByteBuffer可读写总长度 outputBuffer。limit(bufferInfo。offsetbufferInfo。size); 写出 mMediaMuxer。writeSampleData(index,outputBuffer,bufferInfo); } 输出缓冲区我们就使用完了,可以回收了,让mediacodec继续使用 mMediaCodec。releaseOutputBuffer(status,false); 结束 if((bufferInfo。flagsMediaCodec。BUFFERFLAGENDOFSTREAM)!0){ } } } } publicvoidstop(){ isS mHandler。post(newRunnable(){ Override publicvoidrun(){ getCodec(true); mMediaCodec。stop(); mMediaCodec。release(); mMediaC mMediaMuxer。stop(); mMediaMuxer。release(); mMediaM mEglBase。release(); mEglB mInputS mHandler。getLooper()。quitSafely(); mH } }); } EGLBase EGL配置与录制的opengl操作工具类 publicclassEGLBase{ privateScreenFiltermScreenF privateEGLSurfacemEglS privateEGLDisplaymEglD privateEGLConfigmEglC privateEGLContextmEglC paramcontext paramwidth paramheight paramsurfaceMediaCodec创建的surface我们需要将其贴到我们的虚拟屏幕上去 parameglContextGLThread的EGL上下文 publicEGLBase(Contextcontext,intwidth,intheight,Surfacesurface,EGLContexteglContext){ 配置EGL环境 createEGL(eglContext); 把Surface贴到mEglDisplay,发生关系 int〔〕attriblist{ EGL14。EGLNONE }; 绘制线程中的图像就是往这个mEglSurface上面去画 mEglSurfaceEGL14。eglCreateWindowSurface(mEglDisplay,mEglConfig,surface,attriblist,0); 绑定当前线程的显示设备及上下文,之后操作opengl,就是在这个虚拟显示上操作 if(!EGL14。eglMakeCurrent(mEglDisplay,mEglSurface,mEglSurface,mEglContext)){ thrownewRuntimeException(eglMakeCurrent失败!); } 像虚拟屏幕画 mScreenFilternewScreenFilter(context); mScreenFilter。onReady(width,height); } privatevoidcreateEGL(EGLContexteglContext){ 创建虚拟显示器 mEglDisplayEGL14。eglGetDisplay(EGL14。EGLDEFAULTDISPLAY); if(mEglDisplayEGL14。EGLNODISPLAY){ thrownewRuntimeException(eglGetDisplayfailed); } 初始化显示器 int〔〕versionnewint〔2〕; 12。1020203 major:主版本记录在version〔0〕 minor:子版本记录在version〔1〕 if(!EGL14。eglInitialize(mEglDisplay,version,0,version,1)){ thrownewRuntimeException(eglInitializefailed); } egl根据我们配置的属性选择一个配置 int〔〕attriblist{ EGL14。EGLREDSIZE,8,缓冲区中红分量位数 EGL14。EGLGREENSIZE,8, EGL14。EGLBLUESIZE,8, EGL14。EGLALPHASIZE,8, EGL14。EGLRENDERABLETYPE, EGL14。EGLOPENGLES2BIT,egl版本2 EGL14。EGLNONE }; EGLConfig〔〕configsnewEGLConfig〔1〕; int〔〕numconfignewint〔1〕; attriblist:属性列表属性列表的第几个开始 configs:获取的配置(输出参数) numconfig:长度和configs一样就行了 if(!EGL14。eglChooseConfig(mEglDisplay,attriblist,0,configs,0,configs。length,numconfig,0)){ thrownewIllegalArgumentException(eglChooseConfig2failed); } mEglConfigconfigs〔0〕; int〔〕ctxattriblist{ EGL14。EGLCONTEXTCLIENTVERSION,2,egl版本2 EGL14。EGLNONE }; 创建EGL上下文 3sharecontext:共享上下文传绘制线程(GLThread)中的EGL上下文达到共享资源的目的发生关系 mEglContextEGL14。eglCreateContext(mEglDisplay,mEglConfig,eglContext,ctxattriblist,0); 创建失败 if(mEglContextEGL14。EGLNOCONTEXT){ thrownewRuntimeException(EGLContextError。); } } paramtextureId纹理id代表一个图片 paramtimestamp时间戳 publicvoiddraw(inttextureId,longtimestamp){ 绑定当前线程的显示设备及上下文,之后操作opengl,就是在这个虚拟显示上操作 if(!EGL14。eglMakeCurrent(mEglDisplay,mEglSurface,mEglSurface,mEglContext)){ thrownewRuntimeException(eglMakeCurrent失败!); } 画画画到虚拟屏幕上 mScreenFilter。onDrawFrame(textureId); 刷新eglsurface的时间戳 EGLExt。eglPresentationTimeANDROID(mEglDisplay,mEglSurface,timestamp); 交换数据 EGL的工作模式是双缓存模式,内部有两个framebuffer(fb) 当EGL将一个fb显示屏幕上,另一个就在后台等待opengl进行交换 EGL14。eglSwapBuffers(mEglDisplay,mEglSurface); } 回收 publicvoidrelease(){ EGL14。eglDestroySurface(mEglDisplay,mEglSurface); EGL14。eglMakeCurrent(mEglDisplay,EGL14。EGLNOSURFACE,EGL14。EGLNOSURFACE,EGL14。EGLNOCONTEXT); EGL14。eglDestroyContext(mEglDisplay,mEglContext); EGL14。eglReleaseThread(); EGL14。eglTerminate(mEglDisplay); } }