Android 音视频录制硬编码实现

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/

源码

https://github.com/latnok-north/avRecordSample

    原文作者:Murphy_Z
    原文地址: https://www.jianshu.com/p/90c75dab03b4
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞