MediaCodec 从Surface编码及android锁屏录像和后台录像实现

息屏录像模块

基于以下原理, 做出了完整的后头录像锁屏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个线程;

  1. preview 线程一直运行, 从camera获得数据到surfacetexture;
  2. 一个控制线程 Server,也是一直运行, 控制相机初始化,回调,录像源选择,启动录像;
  3. 编码线程实现从surface编码;

预览线程要创建一个”离屏egl surface";
编码线程要从mediacodec返回的surface创建egl surface;
开始录像和停止录像的时候,要让渲染环境在两个线程间互相切换。

离屏渲染

preview 线程做的事情是将Camera预览的内容渲染到一个离屏渲染环境. 如何创建一个离屏渲染环境?

  1. 创建一个offscreen egl surface;
  2. 获得默认display 设备和缓存等;
  3. makeCurrent,使用该surface和egl display设备,缓存等创建egl上下文,这是个共享上下文,可以在多个渲染线程使用;
  4. 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编码?

  1. mediacodec 创建inputsurface;
  2. 设置一个录像线程,但是要求使用同一个shard_context,使用input surface创建EGLSruface;
  3. 初始化openGL,shader等;
  4. makeCurrent, 把当前(录像线程)设置为渲染线程;
  5. 渲染线程只更新surfaceTexture,不画出来,把SurfaceTexture传给录像线程;

参考文档

#1 科学网—EGL资源的数据共享应用和底层驱动实现 - 郭叶军的博文
#2 学习OpenGL-ES: 2 - EGL解析 - kiffa - 博客园

saki4510t/UVCCamera
google/grafika: Grafika test app

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值