Camera预览
目前 Android Camera 有两个版本,分别是Camera 和 Camera2,Camera2 是从 5.0开始引入的,但是由于兼容性问题且很多手机厂商的支持程度比较弱,所以目前还是使用 Camera。
Camera 的预览,先定义了一个 Camera 接口。
interface CameraInterface {
fun openCamera()
fun openCamera(cameraId : Int)
fun releaseCamera()
fun switchCamera(surface: SurfaceTexture)
fun setPreviewDisplay(surface: SurfaceTexture)
fun switchCamera(holder: SurfaceHolder)
fun setPreviewDisplay(holder: SurfaceHolder)
}
要将Camera所捕获的数据渲染在屏幕上,需要有一个承载的地方。所以这个承载的地方可以是SurfaceView、TextureView和GLSurfaceView,本文使用的是GLSurfaceView。
既然是视频那肯定是需要预览的,从Camera提供的方法来看,能获取到预览数据的方式有两种,一种是SurfaceHolder,另一种是SurfaceTexture。本文使用的是GLSurfaceView,那么在此类里面就只能使用SurfaceTexture了。
所以这两个类有什么不一样呢?这里后面再讲。
最后,OpenGLES 也是必不可少的。
使用OpenGLES的话,分三步。
第一步需要先定义一个顶点着色器和一个片段着色器的GLSL。
第二步创建一个顶点着色器对象和一个片段着色器对象,再将各自的glsl代码连接到着色器对象上,再编译着色器对象。
第三步创建一个程序对象,将编译好的着色器对象连接到程序对象上,最后再连接程序对象。
下面是具体的代码。
public static int genProgram(final String strVSource, final String strFSource) {
int iVShader;
int iFShader;
int iProgId;
int[] link = new int[1];
iVShader = loadShader(strVSource, GLES20.GL_VERTEX_SHADER);
if (iVShader == 0) {
Log.d("Load Program", "Vertex Shader Failed");
return 0;
}
iFShader = loadShader(strFSource, GLES20.GL_FRAGMENT_SHADER);
if (iFShader == 0) {
Log.d("Load Program", "Fragment Shader Failed");
return 0;
}
iProgId = GLES20.glCreateProgram();
GLES20.glAttachShader(iProgId, iVShader);
GLES20.glAttachShader(iProgId, iFShader);
GLES20.glLinkProgram(iProgId);
GLES20.glGetProgramiv(iProgId, GLES20.GL_LINK_STATUS, link, 0);
if (link[0] <= 0) {
Log.d("Load Program", "Linking Failed");
return 0;
}
GLES20.glDeleteShader(iVShader);
GLES20.glDeleteShader(iFShader);
return iProgId;
}
private static int loadShader(final String strSource, final int iType) {
int[] compiled = new int[1];
int iShader = GLES20.glCreateShader(iType);
GLES20.glShaderSource(iShader, strSource);
GLES20.glCompileShader(iShader);
GLES20.glGetShaderiv(iShader, GLES20.GL_COMPILE_STATUS, compiled, 0);
if (compiled[0] == 0) {
Log.e("Load Shader Failed", "Compilation\n" + GLES20.glGetShaderInfoLog(iShader));
return 0;
}
return iShader;
}
创建好了programId之后,就可以根据id来获取着色器中的属性
fun init() {
mProgramId = OpenGlUtils.genProgram(VERTEX_SHADER, FRAGMENT_SHADER_EXT)
if (mProgramId <= 0) {
throw RuntimeException("Unable to create program")
}
maPositionLoc = OpenGlUtils.glGetAttribLocation(mProgramId, "aPosition")
OpenGlUtils.checkLocation(maPositionLoc, "aPosition")
maTextureCoordLoc = OpenGlUtils.glGetAttribLocation(mProgramId, "aTextureCoord")
OpenGlUtils.checkLocation(maTextureCoordLoc, "aTextureCoord")
muMVPMatrixLoc = OpenGlUtils.glGetUniformLocation(mProgramId, "uMVPMatrix")
OpenGlUtils.checkLocation(muMVPMatrixLoc, "uMVPMatrix")
muTexMatrixLoc = OpenGlUtils.glGetUniformLocation(mProgramId, "uTexMatrix")
OpenGlUtils.checkLocation(muTexMatrixLoc, "uTexMatrix")
}
接下来到SurfaceTexture,在使用之前,需要先创建一个纹理id
public static int getExternalOESTextureID(){
int[] texture = new int[1];
GLES20.glGenTextures(1, texture, 0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture[0]);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
return texture[0];
}
然后根据生成的纹理id创建一个 SurfaceTexture 的实例,打开Camera,将SurfaceTexture设置为Camera的承载。
mTextureId = OpenGlUtils.getExternalOESTextureID()
mSurfaceTexture = SurfaceTexture(mTextureId) mSurfaceTexture.setOnFrameAvailableListener(onFrameAvailableListener)
mCameraProxy = CameraProxy(CameraCapture())
mCameraProxy.openCamera()
mCameraProxy.setPreviewDisplay(mSurfaceTexture)
OnFrameAvailableListener 的定义如下,这里跟GLSurfaceView所启用的刷新模式有关系。GLSurfaceView.RENDERMODE_CONTINUOUSLY 这个是自动刷新,GLSurfaceView.RENDERMODE_WHEN_DIRTY 这个是通过底层的消息通知上来,让GLSurfaceView 调用刷新。在本文中使用的是GLSurfaceView.RENDERMODE_WHEN_DIRTY,网上的说法是降低cpu 负载,我去测试了一下,使用GLSurfaceView.RENDERMODE_CONTINUOUSLY 的时候,cpu占有率确实高不少。
private val onFrameAvailableListener = SurfaceTexture.OnFrameAvailableListener {
Log.i("GLSurfaceView_History","requestRender")
requestRender()
}
最后就是onDrawFrame的过程了
override fun onDrawFrame(gl: GL10?) {
Log.i("GLSurfaceView_History","onDrawFrame")
mSurfaceTexture.updateTexImage()
...
mSurfaceTexture.getTransformMatrix(mSTMatrix)
mFilter.drawFrame(mTextureId, mSTMatrix)
}
全部源码可以看github,地址在文章末尾。
关于预览角度的问题,从 Camera 出来的预览图像的角度是向右的,所以这里有两个方式处理角度问题。第一个是通过 camera.setDisplayOrientation 可以设置预览角度。第二个是通过纹理坐标。
视频数据获取
从上面的onDrawFrame可以看到,视频帧的获取是通过SurfaceTexture的getTransformMatrix方法获取的。但是这里的获取方式,需要通过和编码器结合起来。
MediaCodec 有两种视频数据的获取方式,第一是使用输出Surface创建另一个绘图表面,第二就是使用ByteBuffer。在使用输出Surface的情况下,则需要创建一个EGLContext和一个新的线程绑定起来,再将视频数据绘制到此Surface上,而在此线程上也要重新初始化顶点着色器和片段着色器相关的东西。
创建另一个绘图表面需要使用到EGL,首先获取到当前GLSurface 的 EGLContext,主要的作用是能够和新创建的EGLContext 共享着色器和纹理。
其实在GLSurfaceView 内部也有EGL的一个创建过程,主要需要调用六个方法,而创建一个新的绘图表面与GLSurfaceView内部的创建过程一致,但是唯一不同的地方就是新的绘图表面在创建EGLContext 的时候与当前的GLSurfaceView共享数据。
1.eglGetDisPlay
2.eglInitialize
3.eglChooseConfig
4.eglCreateContext
5.createWindowSurface
6.eglMakeCurrent
这里需要创建一个新的绘制线程,createWindowSurface的作用是创建一个渲染区域,其中有个参数是MediaCodec提供的Surface,最后MakeCurrent的作用是将EGLContext 与当前线程绑定到一起,使此线程可以绘制。具体源码在EglCore类里面。
然后在onDrawFrame里面将数据通过Handler发送到此线程里面,再去做的绘制到此Surface上。
音频数据获取
AudioRecord 大家都会用了,那开始录音之后的代码是这样的
while (mIsRecording) {
readSize = mAudioRecord.read(tempBuffer, 0, mBufferSize);
if (readSize == AudioRecord.ERROR_INVALID_OPERATION || readSize == AudioRecord.ERROR_BAD_VALUE) {
continue;
}
if (readSize > 0) {
//获取输入buffer的index 内部有同步机制
mAudioInputBufferIndex = mAudioEncoder.getCodec().dequeueInputBuffer(-1);
if (mAudioInputBufferIndex >= 0) {
ByteBuffer inputBuffer = mAudioEncoder.getCodec().getInputBuffer(mAudioInputBufferIndex);
if (inputBuffer != null) {
inputBuffer.put(tempBuffer);
audioAbsolutePtsUs = (System.nanoTime()) / 1000L;
//压入编码栈中
mAudioEncoder.getCodec().queueInputBuffer(mAudioInputBufferIndex, 0, mBufferSize, audioAbsolutePtsUs, 0);
}
}
//通知编码线程获取数据
mAudioEncoder.frameAvailable();
}
}
音频数据压入栈后,通知编码线程做处理,就是这么个过程。假如视频数据不是通过surface获取的,那步骤也和这里一致。
编码
MediaCodec 有三种编码方式,分别是 4.1版本的ByteBuffer的同步方式、5.0版本的同步方式和5.0版本的异步方式,目前我这边使用的是5.0版本的同步方式。
无论是视频还是音频,编码的方式都是一样的,只是创建的MediaCodec的配置信息不一样。所以在此,我定义了一个抽象的编码基类。
下面是具体的获取编码后数据的代码。
private void drainEncoder(boolean endOfStream) {
final int TIMEOUT_USEC = 10000;
String className = this.getClass().getName();
if (endOfStream) {
if (isSurfaceInput()) {
//这个是视频编码结束的标志。
mEncoder.signalEndOfInputStream();
Log.i("lock_thread", "video_end");
}
}
while (true) {
int outputBufferId = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
if (outputBufferId == MediaCodec.INFO_TRY_AGAIN_LATER) {
if (!endOfStream) {
break;
}
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
//当数据开始queue回首先跑到这里,给muxer添加一个通道。且开始合并。
MediaFormat newFormat = mEncoder.getOutputFormat();
mTrackIndex = mMuxer.addTrack(newFormat);
mMuxer.start();
} else if (outputBufferId < 0) {
} else {
//这里是可以真正拿到编码后数据的地方。
ByteBuffer outputBuffer = mEncoder.getOutputBuffer(outputBufferId);
if (mBufferInfo.size != 0 && outputBuffer != null) {
outputBuffer.position(mBufferInfo.offset);
outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
//pts 必须得设置,否则会muxer.stop时抛出异常。
mBufferInfo.presentationTimeUs = getPTSUs();
mMuxer.writeSampleData(mTrackIndex, outputBuffer, mBufferInfo);
prevOutputPTSUs = mBufferInfo.presentationTimeUs;
}
mEncoder.releaseOutputBuffer(outputBufferId, false);
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.i("lock_thread", Thread.currentThread().getName() + "_thread_end");
break;
}
}
}
}
我从google的编码demo中,看到的是在绘制线程中直接编码,这个方式单通道编码出来的文件是没问题的,无论是视频还是音频。但是双通道结合到一起,编码出来的文件就有问题了。所以我换了一个方式去处理这个编码问题,就是增加一个线程去获取编码后的数据写入muxer。
先是在基类实现一个runable,run 方法如下所示
@Override
public void run() {
processWait();
while (mIsCapture) {
drainEncoder(false);
processWait();
if (mIsEndOfStream) {
drainEncoder(true);
release();
releaseMuxer();
}
}
}
当进入这个线程时先wait,等待我有数据写入到encoder 中了,在notify,让这个线程继续走下去。所以什么时候通知编码线程继续往下走呢。当然是视频和音频有数据写入的时候咯。
封装
public WrapMuxer(String outputFile, int tracks) {
try {
mMuxer = new MediaMuxer(outputFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
} catch (IOException e) {
e.printStackTrace();
}
mMaxTracks = tracks;
}
定义了一个 WrapMuxer的类去做封装处理,从上面代码中,无论是视频还是音频,都需要addTrack 然后再 start
所以我在这个类中做了个处理。
public synchronized int addTrack(MediaFormat format) {
mCurrentTracks++;
return mMuxer.addTrack(format);
}
public synchronized void start() {
if (isLoadAllTrack() && !mMuxerStarted) {
mMuxer.start();
mMuxerStarted = true;
}
}
最后release
public void release() {
mCurrentTracks--;
if (mCurrentTracks == 0) {
if (mMuxer != null) {
// TODO: stop() throws an exception if you haven't fed it any data. Keep track
// of frames submitted, and don't call stop() if we haven't written anything.
mMuxer.stop();
mMuxer.release();
mMuxer = null;
}
mMuxerStarted = false;
}
}
最后muxer过程中需要注意一个地方是当没有任何数据通过mMuxer.writeSampleData写入时,最后stop 必定抛异常。
关键属性
MediaCodec.INFO_TRY_AGAIN_LATER //
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED //
引用
https://github.com/google/grafika/
网友评论