系列文章:
安卓特效相機(一) Camera2的使用
安卓特效相機(二) EGL基礎
安卓特效相機(三) OpenGL ES 特效渲染
安卓特效相機(四) 視頻錄制
前幾篇文章已經講完了攝像頭畫面的捕捉和特效渲染,這篇文章我們來講一講最后的視頻錄制部分。
我們這里將使用MediaRecorder去錄制視頻。MediaRecorder可以同時錄制視頻和音頻。我們將音頻源直接設置成攝像頭,讓它從攝像頭里面讀取音頻數據:
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mMediaRecorder.setAudioEncodingBitRate(AUDIO_BIT_RATE);
但是視頻源并不能直接設置成攝像頭,因為攝像頭捕捉到的畫面是原始的視頻畫面,我們上上一篇文章中講到了如何將這個原始畫面繪制到紋理,然后通過特效處理現實到TextureView上:
所以如果我們直接將MediaRecorder的視頻源設置成攝像頭的話錄制下來的視頻并沒有帶上特效。
那要怎么做呢? MediaRecorder有一種視頻源叫做MediaRecorder.VideoSource.SURFACE,意思是從Surface里面讀取畫面去錄制。那我們是不是直接吧TextureView的SurfaceTexture創建的Surface傳給MediaRecorder讓它捕捉TextureView的內容就行了呢?
可惜的是如果直接用MediaRecorder.setInputSurface將Surface設置進去,會拋出異常:
09-22 14:53:47.473 897 943 E AndroidRuntime: java.lang.IllegalArgumentException: not a PersistentSurface
09-22 14:53:47.473 897 943 E AndroidRuntime: at android.media.MediaRecorder.setInputSurface(MediaRecorder.java:165)
原因是只能設置MediaCodec.PersistentSurface類型的Surface:
/**
* Configures the recorder to use a persistent surface when using SURFACE video source.
* <p> May only be called before {@link #prepare}. If called, {@link #getSurface} should
* not be used and will throw IllegalStateException. Frames rendered to the Surface
* before {@link #start} will be discarded.</p>
* @param surface a persistent input surface created by
* {@link MediaCodec#createPersistentInputSurface}
* @throws IllegalStateException if it is called after {@link #prepare} and before
* {@link #stop}.
* @throws IllegalArgumentException if the surface was not created by
* {@link MediaCodec#createPersistentInputSurface}.
* @see MediaCodec#createPersistentInputSurface
* @see MediaRecorder.VideoSource
*/
public void setInputSurface(@NonNull Surface surface) {
if (!(surface instanceof MediaCodec.PersistentSurface)) {
throw new IllegalArgumentException("not a PersistentSurface");
}
native_setInputSurface(surface);
}
好吧直接滴干活不行那我們就悄悄滴干活。
首先還是需要視頻源設置成MediaRecorder.VideoSource.SURFACE,然后配置一堆的視頻信息。這些設置項具體是什么意思講起來比較費勁,我就不展開了,大家感興趣的可以自行搜索:
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mMediaRecorder.setOutputFile(mLastVideo.getPath());
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mMediaRecorder.setVideoEncodingBitRate(VIDEO_BIT_RATE);
mMediaRecorder.setVideoSize(mPreview.getWidth(), mPreview.getHeight());
mMediaRecorder.setVideoFrameRate(VIDEO_FRAME_RATE);
mMediaRecorder.setOrientationHint(0);
配置完之后開啟錄制:
try {
mMediaRecorder.prepare();
} catch (IOException e) {
Toast.makeText(this, "failed to prepare MediaRecorder", Toast.LENGTH_LONG)
.show();
}
mMediaRecorder.start();
上面的都是一些常規操作,大部分使用MediaRecorder的代碼都是這樣用的,下面我們來看正片:
return mGLRender.createEGLSurface(mMediaRecorder.getSurface());
這里拿到MediaRecorder的那個視頻源Surface,給它創建了一個EGLSurface。我們在之前那篇EGL基礎里面介紹過它。
我們可以用EGL14.eglMakeCurrent方法指定OpenGL往哪個Surface里面繪制,所以我們直接修改代碼將OpenGL的目標Suface設置成這個視頻源Surface就可以了嗎?
恭喜你,得到了一個BUG。
現在視頻是可以錄制了,但是預覽畫面黑了。為什么,回顧下這幅圖:
我們需要要將OpenGL的畫面繪制到TextureView上才能在屏幕上看到特效渲染后的預覽畫面。
那怎么辦?TextureView和MediaRecorder只能二選一了嗎?不,小孩子才做選擇題,成年人當然是全都要。
我們讓OpengGL辛苦點,畫兩次...
首先修改下GLRender.render方法, EGLSurface由外面傳進來,這樣我們就能在外面控制它往TextureView和MediaRecord繪制了:
public void render(float[] matrix, EGLSurface eglSurface) {
makeCurrent(eglSurface);
GLES20.glUniformMatrix4fv(mTransformMatrixId, 1, false, matrix, 0);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES11Ext.GL_SAMPLER_EXTERNAL_OES, mGLTextureId);
GLES20.glUniform1i(mTexPreviewId, 0);
GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glDrawElements(GLES20.GL_TRIANGLES, ORDERS.length, GLES20.GL_UNSIGNED_SHORT, mOrder);
EGL14.eglSwapBuffers(mEGLDisplay, eglSurface);
}
然后在繪制的時候繪制兩次:
mCameraTexture.updateTexImage();
mCameraTexture.getTransformMatrix(mTransformMatrix);
mGLRender.render(mTransformMatrix, mGLRender.getDefaultEGLSurface());
if (mRecordSurface != null) {
mGLRender.render(mTransformMatrix, mRecordSurface);
mGLRender.setPresentationTime(mRecordSurface, mCameraTexture.getTimestamp());
}
這里需要注意的是我們需要給這一幀設置下時間戳,用于錄制視頻的時間同步:
public void setPresentationTime(EGLSurface eglSurface, long nsecs) {
EGLExt.eglPresentationTimeANDROID(mEGLDisplay, eglSurface, nsecs);
}
好了,這個錄像的實現方法比較簡單。到此整個特效相機的教程就結束了,希望對大家有用。
這篇文章的demo依然在github(注意是feature_record分支)