我们先来温故一下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总算完结了。下一步,也是最后一步,让我们开始使用它吧。
第一步,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.