息屏录像模块
基于以下原理, 做出了完整的后头录像锁屏app.不同于其它监控软件的伪后台(伪装窗口运行),此模块完全后台运行,即使锁屏状态也能监测和录像. 支持高清录像/录音/选择摄像头.
锁屏app介绍
当前功能有:
移动侦测录像. 此软件可以侦测摄像头范围内画面.当发生画面变化时自动开始录像,当动作停止一分钟后自动停止录像并保存;
晃动手机启动录像;
可定制做 脸部识别侦测录像.侦测到人脸时自动开始录像. 用usb红外摄像头作为输入源(已测试部分设备可行), 录像后自动上传到网盘, 多路摄像头录像. 这些功能demo做过, 但是没时间集成到这个应用.
使用场景:
可用于室内防盗/宠物记录/隐蔽拍摄等.
后台录像已做成 androidStudio 模块. 直接调用使用. 需要此模块请私信或联系邮箱 ilotuo@163.com 咨询.
演示视频
http://v.youku.com/v_show/id_XMTY3NTA1NDk4MA==.html?spm=a2hzp.8253869.0.0.4C43Bp&from=y1.7-2
整体框架原理
模块录像时一共有3个线程;
- preview 线程一直运行, 从camera获得数据到surfacetexture;
- 一个控制线程 Server,也是一直运行, 控制相机初始化,回调,录像源选择,启动录像;
- 编码线程实现从surface编码;
预览线程要创建一个”离屏egl surface";
编码线程要从mediacodec返回的surface创建egl surface;
开始录像和停止录像的时候,要让渲染环境在两个线程间互相切换。
离屏渲染
preview 线程做的事情是将Camera预览的内容渲染到一个离屏渲染环境. 如何创建一个离屏渲染环境?
- 创建一个offscreen egl surface;
- 获得默认display 设备和缓存等;
- makeCurrent,使用该surface和egl display设备,缓存等创建egl上下文,这是个共享上下文,可以在多个渲染线程使用;
- opengl初始化,纹理初始化,surfacetexture初始化,得到对应surface。
关于egl是什么见引用文章。
Android从Surface编码原理
这是api 18之后的功能。下面尝试把手机摄像头preview渲染到Input Surface.然后对该Surface编码.
首先该Surface由MediaCodec创建:
mSurface = mMediaCodec.createInputSurface(); // API >= 18
然后手动为该Surface初始化EGL后,渲染GL画面,编码。
add on 06/30/2016 SurfaceEncode Example .
编码输出
先看编码是怎么输出的.典型的MediaCodec Buffer编码,dequeueOutputBuffer的流水线顺序是这样:
1: changeFormat .
2: deque sps,pps buffer . BufferInfo 的flags被置位为:BUFFER_FLAG_CODEC_CONFIG.(如果是编码为h264,此时应写入h264文件)
3: IFrame (2+3 = IDR Frame)
4: many pFrame …
但是当使用InputSurface时,第二步被跳过,也就是没有BUFFER_FLAG_CODEC_CONFIG buffer,此时应该手动取sps和pps :
ByteBuffer sps = newFormat.getByteBuffer("csd-0");
ByteBuffer pps = newFormat.getByteBuffer("csd-1");
如果是H264裸流要先把这两个buffer写到文件头或推流.
详见SufraceEncoder.md drainAllEncoderMuxer函数.
初始化EGL
本例以手机摄像头录像为例. 下面的代码来自SufraceEncoder.md(上一节有开源链接). 在preview渲染线程增加录像线程:
mSurfaceEncoder = new SurfaceEncoder(thread.mVideoSource.mCols,thread.mVideoSource.mRows);
Surface sur = mSurfaceEncoder.getInputSurface();
mRendererHolder.setRecordingSurface(sur);
其中 mRendererHolder为preivew主线程.
seRecordingSSurface函数:
public void setRecordingSurface(final Surface surface){
RecordSurfaceRenderHandler rh = RecordSurfaceRenderHandler.createHandler();
rh.setEglContext(mMasterEgl.getContext(), mTexId, surface, true);
mClients.add(rh);
}
RecordSurfaceRenderHandler 完成创建录制线程,为Surafce初始化EGL,在帧更新回调时切换EGL Context,并绘制到Surface.
源码 RecordSurfaceRenderHandler.md
关于EGL初始化,首先了解下EGL是什么,我这里看过两篇文章讲的比较好. 引用文章[1]和[2]
第一篇讲EGL原理,本质.第二篇将EGL应用.
通过以上文章我们知道,我们将要共享一块纹理, 那么EGL context 是唯一. 任何EGL Surface 渲染前都要和这个Context 绑定.使成为渲染目标.如果你的渲染主线程来自GLSurfaceView ,那么要先获得它的EGLContext, 我还没有实践过,也许可以参考这个提问:android - How can GLSurfaceView use my EGLDisplay, EGLContext and eglSurface? - Stack Overflow
handleSetEglContext 函数为surface初始化EGL环境,EGLBase在grafika工程有,过程和原理结合源码参考上面的#2链接.可以认为其所做的都是为makeCurrent(切换渲染对象)时做准备. EGLBase#makeCurrent实现就调用了一句:
EGL14.eglMakeCurrent(mEglDisplay, surface, surface, mEglContext)
将共享Context和我们的Surface绑定在一起.
Draw
把编码线程当成主线程的一个client,在主渲染线程帧回调时,向编码线程发送消息:
handler.sendMessage(handler.obtainMessage(MSG_RENDER_DRAW2, (int) (timestamp >> 32), (int) timestamp,mTxtMat));
编码线程的处理: GLDrawer2D 负责编译shader 和调用GLES接口进行渲染,以及Surface swap交换帧缓存.每次更新自动给MediaCodec输入帧数据.RencordSurfaceRenderHandler#handleFrameAvailable 完成渲染和drain编码:
private void handleFrameAvailable(int tex_id,float[] transform, long timestampNanos) {
Log.v(TAG,"handleFrameAvailable #0");
SurfaceEncoder mVideoEncoder = SurfaceEncoder.getInstance();
if(mVideoEncoder==null || !mVideoEncoder.isRecording())
return;
Log.d(TAG, "handleDrain: #3");
mVideoEncoder.drainAllEncoderMuxer(false);
mDrawer.draw(tex_id, transform);
mTargetSurface.setPresentationTime(timestampNanos);
mTargetSurface.swap();
Log.v(TAG,"handleFrameAvailable #1");
}
mVideoEncoder.drainAllEncoderMuxer 编码输出前面讲过了,和BufferInput差不多,去掉Buffer输入和注意sps和pps保存即可.
录制线程的帧回调只是多了mTargetSurface.swap().其作用就是调用GL14.eglSwapBuffers(mEglDisplay, surface) 将画到EglDisplay上的缓存换到Surface的帧缓存,录制线程的绘制.
小结
如何从surface编码?
- mediacodec 创建inputsurface;
- 设置一个录像线程,但是要求使用同一个shard_context,使用input surface创建EGLSruface;
- 初始化openGL,shader等;
- makeCurrent, 把当前(录像线程)设置为渲染线程;
- 渲染线程只更新surfaceTexture,不画出来,把SurfaceTexture传给录像线程;
参考文档
#1 科学网—EGL资源的数据共享应用和底层驱动实现 - 郭叶军的博文
#2 学习OpenGL-ES: 2 - EGL解析 - kiffa - 博客园