OpenGL.ES在Android上的简单实践:22-水印录制(MediaCodec输出h264+MediaMuxer合成mp4 下)

OpenGL.ES在Android上的简单实践:22-水印录制(MediaCodec输出h264+MediaMuxer合成mp4 下)

 

1、Inner CameraRecordEncoder

我们先来温故一下CameraRecordEncoder的设计逻辑,其代码组成分为 编码录制工作线程,和供外部请求操作的方法。以请求开始录制方法startRecording为例。

public class CameraRecordEncoder implements Runnable {   
// ---------------以下代码 供外部线程通信访问 CameraRecordEncoder工作线程不直接使用--------------------------------------
   /**
     * 开始视频录制。(一般是从其他非录制现场调用的)
     * 我们创建一个新线程,并且根据传入的录制配置EncoderConfig创建编码器。
     * 我们挂起线程等待正式启动后才返回。
     */
    public void startRecording(EncoderConfig encoderConfig) {
        Log.d(TAG, "CameraRecordEncoder: startRecording()");
        synchronized (mSyncLock) {
            if (mRunning) {
                Log.w(TAG, "Encoder thread already running");
                return;
            }
            mRunning = true;
            new Thread(this, "CameraRecordEncoder").start();
            while (!mReady) {
                try {
                    // 等待编码器线程的启动
                    mSyncLock.wait();
                } catch (InterruptedException ie) {
                    ie.printStackTrace();
                }
            }
        }
        mHandler.sendMessage(mHandler.obtainMessage(EncoderHandler.MSG_START_RECORDING, encoderConfig));
    }

    @Override
    public void run() {
        Looper.prepare();
        synchronized (mSyncLock) {
            mHandler = new EncoderHandler(this);
            mReady = true;
            mSyncLock.notify();
        }
        Looper.loop();

        Log.d(TAG, "Encoder thread exiting");
        synchronized (mSyncLock) {
            mReady = mRunning = false;
            mHandler = null;
        }
    }
// ---------------以上代码 供外部线程通信访问 CameraRecordEncoder工作线程不直接使用--------------------------------------
   Handler代码省略,详情follow github ...
// ---------------以下部分代码 仅由编码器线程访问 ---------------------------------------------------------------------
    private int mTextureId;
    private int mSignTexId;
    private CameraRecordEncoderCore mRecordEncoder;
    private EglCore mEglCore;
    private WindowSurface mRecorderInputSurface;
    private FrameRect mFrameRect;
    private WaterSignature mWaterSign;

    private void handleStartRecording(EncoderConfig config) {
        Log.d(TAG, "handleStartRecording " + config);
        try {
            // 初始化我们的编码工具类
            mRecordEncoder = new CameraRecordEncoderCore(config.mWidth, config.mHeight,
                    config.mBitRate, config.mOutputFile);
            // 初始化我们的水印签名纹理
            mSignTexId = TextureHelper.loadTexture(config.mContext, R.mipmap.name);
        } catch (IOException ioe) {
            throw new RuntimeException(ioe);
        }
        // 初始化录制渲染的EGL环境
        mEglCore = new EglCore(config.mEglContext, EglCore.FLAG_RECORDABLE);
        // 获取录制编码器的输入Surface,创建成EGLSurface
        mRecorderInputSurface = new WindowSurface(mEglCore, mRecordEncoder.getInputSurface(), true);
        mRecorderInputSurface.makeCurrent();
        // 创建录制用的 摄像头预览ShaderProgram
        mFrameRect = new FrameRect();
        mFrameRect.setShaderProgram(new FrameRectSProgram());
        // 创建录制用的 水印签名ShaderProgram
        mWaterSign = new WaterSignature();
        mWaterSign.setShaderProgram(new WaterSignSProgram());
    }

当外部使用者在主线程调用startRecording的时,我们创建录制编码线程,通过Handler机制触发 录制编码器的初始化工作,和其他一系列所需要的部分初始化工作。 请一定要和ContinuousRecordActivity实时预览摄像头那边的EGL分开理解,虽然他们看起来几乎一样,但两者的区别就在WindowSurface。(以下语句有点绕口)实时预览摄像头的WindowSurface的Surface来源是SurfaceView,是用于显示的;而编码器的WindowSurface的Surface来源是Codec的输入Surface,是用于给编码器喂养的。请一定要搞清楚!!!

如果同学们已经理解掌握到以上的重点,那下面的代码已经难不倒你了。我们看看视频渲染FrameRect和水印签名WaterSignature是怎么每帧每帧的流进Codec。

// ---------------以下代码 供外部线程通信访问 CameraRecordEncoder工作线程不直接使用--------------------------------------    
    public void frameAvailable(SurfaceTexture mSurfaceTexture) {
        synchronized (mSyncLock) {
            if (!mReady) {
                return;
            }
        }
        Matrix.setIdentityM(transform, 0);
        mSurfaceTexture.getTransformMatrix(transform);
        long timestamp = mSurfaceTexture.getTimestamp();
        // This timestamp is in nanoseconds!纳秒为单位
        if (timestamp == 0) {
            // 调试发现当按下开关,关闭打开屏幕的时候,会遇到 PresentationTime=0
            Log.w(TAG, "NOTE: got SurfaceTexture with timestamp of zero");
            return;
        }
        // 我这里是纠结的,如果想startRecord自定义一个传递bean,或者直接传递SurfaceTexture,都显得太重了。
        // 因为此方法是每帧都调用,尽量轻量高效。
        mHandler.sendMessage(mHandler.obtainMessage(EncoderHandler.MSG_FRAME_AVAILABLE,
                (int) (timestamp >> 32), (int) timestamp, transform));
    }

    public void setTextureId(int id) {
        synchronized (mSyncLock) {
            if (!mReady) {
                return;
            }
        }
        mHandler.sendMessage(mHandler.obtainMessage(EncoderHandler.MSG_SET_TEXTURE_ID, id, 0, null));
    }
// ---------------以上代码 供外部线程通信访问 CameraRecordEncoder工作线程不直接使用--------------------------------------
   Handler代码省略,详情follow github ...
// ---------------以下部分代码 仅由编码器线程访问 ---------------------------------------------------------------------
    private int mTextureId;    
    // 设置摄像头的帧纹理
    private void handleSetTexture(int id) {
        Log.d(TAG, "handleSetTexture " + id);
        mTextureId = id;
    }
    private void handleFrameAvailable(float[] transform, long timestampNanos) {
        //先推动一次编码器把编码数据写入MP4
        mRecordEncoder.drainEncoder(false);
        
        mRecorderInputSurface.makeCurrent();
        GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
        GLES20.glEnable(GLES20.GL_BLEND);
        GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        GLES20.glViewport(0, 0, 
                mRecorderInputSurface.getWidth(),
                mRecorderInputSurface.getHeight() );
        mFrameRect.drawFrame(mTextureId, transform);
        GLES20.glViewport(0, 0, 288, 144);
        mWaterSign.drawFrame(mSignTexId);
        // mRecorderInputSurface是 获取编码器的输入Surface 创建的EGLSurface,
        // 以上draw的内容直接渲染到mRecorderInputSurface,喂养数据到编码器当中,非常方便。
        mRecorderInputSurface.setPresentationTime(timestampNanos);
        mRecorderInputSurface.swapBuffers();
    }

这里出现一个概念知识点,就是每帧的显示时间(Surface.PresentationTime)这个概念是渲染视频通用的,具体概括就是:视频每一帧的图画都有自己对应的显示时间戳。如果我们把原本的正常时间戳放大,那就对应我们观看视频的放慢操作;时间戳缩小,那就对应观看视频的加速操作。这个时间戳在音视频同步的矫正尤为关键,以后我们在讨论。

从代码注释可以知道,当外部使用者在主线程调用frameAvailable时候,这部分我是很纠结的,如果我们像startRecord自定义一个传递bean,或者直接传递SurfaceTexture,都显得太重了。因为frameAvailable方法是每帧都调用,我们应尽量轻量且高效。更重要的是,Android系统SurfaceTexture获取的帧时间戳是以纳秒为单位的!我必须把这个long拆分成一个高位的int和低位的int,分别通过handler的arg1和arg2传递到编码工作线程,然后再组装回来(痛苦.jpg)。除了这个时间戳,我们还需要关注摄像头帧纹理mTextureId 和 其变换矩阵TransformMatrix的传递,我们都用Handler机制传递到编码工作线程。

然后来到编码工作线程,我们调用mRecordEncoder.drainEncoder先推动一次编码器把编码数据写入MP4,然后在编码录制的EGLSurface环境下进行绘制操作。记得通过我提供的EglCore.setPresentationTime设置EGLSurface的帧显示时间戳。(具体代码请follow github,这部分是干货来,嘿嘿嘿~)

 

长按录制按钮松开后,我们就需要告知编码器终结编码,完成MP4的合成,回收一切可回收的资源,退出工作线程了。

// ---------------以下代码 供外部线程通信访问 CameraRecordEncoder工作线程不直接使用--------------------------------------   
   /**
     * 告诉录像渲染线程停止录像  (一般是从其他非录制现场调用的)
     */
    public void stopRecording() {
        mHandler.sendMessage(mHandler.obtainMessage(EncoderHandler.MSG_STOP_RECORDING));
        mHandler.sendMessage(mHandler.obtainMessage(EncoderHandler.MSG_QUIT));
        // Codec和Muxer感觉不是立刻结束的,我们是不是应该弄个回调?
    }
    class EncoderHandler extends Handler {
        ... ... 
        @Override
        public void handleMessage(Message msg) {
            ... ...
            int what = msg.what;
            Object obj = msg.obj;
            switch (what) {
                ... ...
                case MSG_QUIT:
                    Looper.myLooper().quit();
                // 不能直接在stopRecording中quit,因为调用stopRecording的looper不是我们想退出的线程looper。  
                    break;
                ... ...
            }
        }
    }
// ---------------以上代码 供外部线程通信访问 CameraRecordEncoder工作线程不直接使用--------------------------------------

// ---------------以下部分代码 仅由编码器线程访问 ----------------------------------------------------------------------
    private void handleStopRecording() {
        Log.d(TAG, "handleStopRecording");
        // true发送编码终结符给编码器,等待合成MP4结束
        mRecordEncoder.drainEncoder(true);
        releaseEncoder();
    }

    private void releaseEncoder() {
        mRecordEncoder.release();
        if (mRecorderInputSurface != null) {
            mRecorderInputSurface.release();
            mRecorderInputSurface = null;
        }
        if (mEglCore != null) {
            mEglCore.release();
            mEglCore = null;
        }
    }

呼~CameraRecordEncoder总算完结了。下一步,也是最后一步,让我们开始使用它吧。

 

2、Using CameraRecordEncoder

第一步,ContinuousRecordActivity.onCreate创建CameraRecordEncoder,并在视图布局增加一个ImageView充当静默录制按下抬手的手指操作。并且创建我们录制MP4的输出文件。代码如下:

public class ContinuousRecordActivity 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ... ... ...
        mRecordEncoder = new CameraRecordEncoder(); // 创建编码录制器
        ImageView btnRecord = (ImageView) findViewById(R.id.btn_record);
        btnRecord.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = event.getAction();
                switch (action) {
                    case MotionEvent.ACTION_UP:
                        mRequestRecord = false; //抬手,请求 停止录制
                        break;
                    case MotionEvent.ACTION_DOWN:
                        mRequestRecord = true; //按下,请求 开始录制
                        break;
                }
                return true;
            }
        });
        // 录制MP4的输出路径
        outputFile = new File(Environment.getExternalStorageDirectory().getPath(), "camera-test.mp4");
    }
}

第二步,在摄像头预览准备期间,先获取编码录制器的运行状态

    @Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {
        mTextureId = GlUtil.createExternalTextureObject();
        mCameraTexture = new SurfaceTexture(mTextureId);
        ... ... 
        recording = mRecordEncoder.isRecording();
    }

第三步,也是最重要的一步,在摄像头预览帧绘制函数中,根据条件操作编码录制器。

    private void drawFrame() {
        mDisplaySurface.makeCurrent();
        ... ...
        mDisplaySurface.swapBuffers();

        // 水印录制 状态设置
        if(mRequestRecord) {
            if(!recording) {
                mRecordEncoder.startRecording(new CameraRecordEncoder.EncoderConfig(
                        outputFile, VIDEO_HEIGHT, VIDEO_WIDTH, 1000000,
                        EGL14.eglGetCurrentContext(), ContinuousRecordActivity.this));

                mRecordEncoder.setTextureId(mTextureId);
                recording = mRecordEncoder.isRecording();
            }
            // mRecordEncoder.setTextureId(mTextureId);
            mRecordEncoder.frameAvailable(mCameraTexture);
        } else {
            if(recording) {
                mRecordEncoder.stopRecording();
                recording = false;
            }
        }
    }

逻辑比较简单,我想大家应该都能看的懂。根据用户的操作mRequestRecord 和 编码器实际运行的状态recording 我们可以轻松的做到按钮按着录制,抬手结束。 OK! 拿去对着邻居女孩用吧,用着用着你就会发现。。。还是有坑啊(奸笑.jpg)

 

总结:我们已经成功在摄像头预览的同时,开启另外一个EGLSurface进行录制。希望大家能通过这个例子明白,我们自定义的EglCore和WindowSurface是多么的强大和方便。 在我看来,全网上还真没有几个的静默录制,特效处理比我这个高效,原生和方便。(牛吹得有点高了)喜欢的同学 follow github

https://github.com/MrZhaozhirong/BlogApp 

The End.

你可能感兴趣的:(OpenGL.ES在Android上的简单实践:22-水印录制(MediaCodec输出h264+MediaMuxer合成mp4 下))