上文中我們已經(jīng)實(shí)現(xiàn)了在紋理上添加濾鏡的效果。這編文章就是將OpenGl和相機(jī)結(jié)合到一起摆出。
預(yù)覽與拍照
整體流程理解
- 將
Camera
中得到的ImageStream
由SurfaceTexture
接受葫掉,并轉(zhuǎn)換成OpenGL ES
紋理。 - 創(chuàng)建
GLSurfaceView
。在OpenGL
環(huán)境下,用GLSurfaceView.Render
將這個紋理繪制出來比伏。 - 整體的
ImageStream
的流向就是
Camera ==>SurfaceTexture==>texture(samplerExternalOES) ==>draw to GLSurfaceView
各個部分詳解
Camra Api
首先是相機(jī)的Api的書寫。
Camera Interface
為我們相機(jī)的操作定義一個接口疆导。因?yàn)槲覀兊南鄼C(jī)Api赁项。有Camera2和Camera的區(qū)別。這里還是簡單的使用Camera來完成澈段。
/**
* 定義個相機(jī)的功能接口
*/
public interface ICamera {
boolean open(int cameraId);
/**
* 設(shè)置畫面的比例
*/
void setAspectRatio(AspectRatio aspectRatio);
/**
* 開啟預(yù)覽
*/
boolean preview();
/**
* 關(guān)閉相機(jī)
*
* @return
*/
boolean close();
/**
* 使用SurfaceTexture 來作為預(yù)覽的畫面
*
* @param surfaceTexture
*/
void setPreviewTexture(SurfaceTexture surfaceTexture);
CameraSize getPreviewSize();
CameraSize getPictureSize();
}
定義一個相機(jī)的接口悠菜。我們知道。我們需要相機(jī)做的幾個通常的操作均蜜。
CameraApi14
/**
* for api 14
* <p>
* Camera主要涉及參數(shù)
* 1. 預(yù)覽畫面的大小
* 2. pic圖片的大小
* 3. 對焦模式
* 4. 閃光燈模式
*/
public class CameraApi14 implements ICamera {
/*
當(dāng)前的相機(jī)Id
*/
private int mCameraId;
/*
當(dāng)前的相機(jī)對象
*/
private Camera mCamera;
/*
當(dāng)前的相機(jī)參數(shù)
*/
private Camera.Parameters mCameraParameters;
//想要的尺寸李剖。
private int mDesiredHeight = 1920;
private int mDesiredWidth = 1080;
private boolean mAutoFocus;
public CameraSize mPreviewSize;
public CameraSize mPicSize;
/*
* 當(dāng)前相機(jī)的高寬比
*/
private AspectRatio mDesiredAspectRatio;
public CameraApi14() {
mDesiredHeight = 1920;
mDesiredWidth = 1080;
//創(chuàng)建默認(rèn)的比例.因?yàn)楹笾脭z像頭的比例,默認(rèn)的情況下囤耳,都是旋轉(zhuǎn)了270
mDesiredAspectRatio = AspectRatio.of(mDesiredWidth, mDesiredHeight).inverse();
}
@Override
public boolean open(int cameraId) {
/*
預(yù)覽的尺寸和照片的尺寸
*/
final CameraSize.ISizeMap mPreviewSizes = new CameraSize.ISizeMap();
final CameraSize.ISizeMap mPictureSizes = new CameraSize.ISizeMap();
if (mCamera != null) {
releaseCamera();
}
mCameraId = cameraId;
mCamera = Camera.open(mCameraId);
if (mCamera != null) {
mCameraParameters = mCamera.getParameters();
mPreviewSizes.clear();
//先收集參數(shù).因?yàn)槊總€手機(jī)能夠得到的攝像頭參數(shù)都不一致篙顺。所以將可能的尺寸都得到。
for (Camera.Size size : mCameraParameters.getSupportedPreviewSizes()) {
mPreviewSizes.add(new CameraSize(size.width, size.height));
}
mPictureSizes.clear();
for (Camera.Size size : mCameraParameters.getSupportedPictureSizes()) {
mPictureSizes.add(new CameraSize(size.width, size.height));
}
//挑選出最需要的參數(shù)
adJustParametersByAspectRatio2(mPreviewSizes, mPictureSizes);
return true;
}
return false;
}
private void adJustParametersByAspectRatio(CameraSize.ISizeMap previewSizes, CameraSize.ISizeMap pictureSizes) {
//得到當(dāng)前預(yù)期比例的size
SortedSet<CameraSize> sizes = previewSizes.sizes(mDesiredAspectRatio);
if (sizes == null) { //表示不支持.
// TODO: 2018/9/14 這里應(yīng)該拋出異常充择?
return;
}
//當(dāng)前先不考慮Orientation
CameraSize previewSize;
mPreviewSize = new CameraSize(mDesiredWidth, mDesiredHeight);
if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) {
mPreviewSize = new CameraSize(mDesiredHeight, mDesiredWidth);
mCameraParameters.setRotation(90);
} else {
// previewSize = mPreviewSize;
}
//默認(rèn)去取最大的尺寸
mPicSize = pictureSizes.sizes(mDesiredAspectRatio).first();
mCameraParameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
mCameraParameters.setPictureSize(mPicSize.getWidth(), mPicSize.getHeight());
//設(shè)置對角和閃光燈
setAutoFocusInternal(mAutoFocus);
//先不設(shè)置閃光燈
// mCameraParameters.setFlashMode("FLASH_MODE_OFF");
//設(shè)置到camera中
// mCameraParameters.setRotation(90);
mCamera.setParameters(mCameraParameters);
// mCamera.setDisplayOrientation(90);
// setCameraDisplayOrientation();
}
private void adJustParametersByAspectRatio2(CameraSize.ISizeMap previewSizes, CameraSize.ISizeMap pictureSizes) {
//得到當(dāng)前預(yù)期比例的size
SortedSet<CameraSize> sizes = previewSizes.sizes(mDesiredAspectRatio);
if (sizes == null) { //表示不支持.
// TODO: 2018/9/14 這里應(yīng)該拋出異常德玫?
return;
}
//當(dāng)前先不考慮Orientation
mPreviewSize = sizes.first();
//默認(rèn)去取最大的尺寸
mPicSize = pictureSizes.sizes(mDesiredAspectRatio).first();
mCameraParameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
mCameraParameters.setPictureSize(mPicSize.getWidth(), mPicSize.getHeight());
mPreviewSize = mPreviewSize.inverse();
mPicSize = mPicSize.inverse();
//設(shè)置對角和閃光燈
setAutoFocusInternal(mAutoFocus);
//先不設(shè)置閃光燈
// mCameraParameters.setFlashMode("FLASH_MODE_OFF");
//設(shè)置到camera中
// mCameraParameters.setRotation(90);
mCamera.setParameters(mCameraParameters);
// mCamera.setDisplayOrientation(90);
// setCameraDisplayOrientation();
}
private boolean setAutoFocusInternal(boolean autoFocus) {
mAutoFocus = autoFocus;
// if (isCameraOpened()) {
final List<String> modes = mCameraParameters.getSupportedFocusModes();
if (autoFocus && modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
} else if (modes.contains(Camera.Parameters.FOCUS_MODE_FIXED)) {
mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_FIXED);
} else if (modes.contains(Camera.Parameters.FOCUS_MODE_INFINITY)) {
mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY);
} else {
mCameraParameters.setFocusMode(modes.get(0));
}
return true;
// } else {
// return false;
// }
}
private void releaseCamera() {
if (mCamera != null) {
mCamera.release();
mCamera = null;
}
}
@Override
public void setAspectRatio(AspectRatio aspectRatio) {
this.mDesiredAspectRatio = aspectRatio;
}
@Override
public boolean preview() {
if (mCamera != null) {
mCamera.startPreview();
return true;
}
return false;
}
@Override
public boolean close() {
if (mCamera != null) {
try {
//stop preview時,可能爆出異常
mCamera.stopPreview();
mCamera.release();
mCamera = null;
return true;
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
@Override
public void setPreviewTexture(SurfaceTexture surfaceTexture) {
if (mCamera != null) {
try {
mCamera.setPreviewTexture(surfaceTexture);
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public CameraSize getPreviewSize() {
return mPreviewSize;
}
@Override
public CameraSize getPictureSize() {
return mPicSize;
}
}
這里的代碼就是使用Camera
來實(shí)現(xiàn)上面的功能椎麦。
- 因?yàn)槭褂梦覀兤谕麑amera中得到的數(shù)據(jù)傳遞到紋理上宰僧,所以需要
setPreviewTexture(SurfaceTexture texture)
。讓這個SurfaceTexture
來承載观挎。
@Override
public void setPreviewTexture(SurfaceTexture surfaceTexture) {
if (mCamera != null) {
try {
mCamera.setPreviewTexture(surfaceTexture);
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 選擇相機(jī)的預(yù)覽的尺寸和旋轉(zhuǎn)的角度
相機(jī)的parameter
的選擇琴儿,只要選對了對應(yīng)的想要的比例就行了。沒有其他需要的嘁捷。
設(shè)備坐標(biāo)和紋理坐標(biāo)之間的方向不同問題造成,由后面紋理的矩陣來控制就好了。
SurfaceTexture
可以從圖像流中捕獲幀作為OpenGL ES
紋理雄嚣。
- 直接使用創(chuàng)建的紋理晒屎,來創(chuàng)建SurfaceTexture就可以了。
mSurfaceTexture = new SurfaceTexture(mTextureId);
- 然后再將其設(shè)置給Camera.同時每次SurfaceTexture刷新的時候缓升,都必須刷新GLSurfaceView鼓鲁。
mCameraApi.setPreviewTexture(mCameraDrawer.getSurfaceTexture());
//默認(rèn)使用的GLThread.每次刷新的時候,都強(qiáng)制要求是刷新這個GLSurfaceView
mCameraDrawer.getSurfaceTexture().setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
requestRender();
}
});
注意事項
使用時必須要注意的是
-
紋理對象使用
GL_TEXTURE_EXTERNAL_OES
紋理目標(biāo)港谊,該目標(biāo)由GL_OES_EGL_image_externalOpenGL ES擴(kuò)展定義骇吭。
每次綁定紋理時,它必須綁定到GL_TEXTURE_EXTERNAL_OES
目標(biāo)而不是GL_TEXTURE_2D
目標(biāo)封锉。在OpenGL ES 2.0著色器必須使用
#extension GL_OES_EGL_image_external:require
- 著色器還必須使用samplerExternalOES GLSL采樣器類型訪問紋理绵跷。
uniform samplerExternalOES uTexture;
GLSurfaceView.Render
GLSL部分
- oes_base_vertex.glsl
attribute vec4 aPosition;
attribute vec2 aCoordinate;
uniform mat4 uMatrix;
uniform mat4 uCoordinateMatrix;
varying vec2 vTextureCoordinate;
void main(){
gl_Position = uMatrix*aPosition;
vTextureCoordinate = (uCoordinateMatrix*vec4(aCoordinate,0.1,0.1)).xy;
}
頂點(diǎn)著色器對比相對簡單膘螟。這里需要注意的就是矩陣相乘的順序問題。
//這個是正確的順序碾局。相稱的順序相反荆残,圖像是反的!>坏薄内斯!
gl_Position = uMatrix*aPosition;
-
oes_base_fragment.glsl
這里就是如上面注意事項中說的。必須使用samplerExternalOES
來采樣像啼。
#extension GL_OES_EGL_image_external : require
precision mediump float;
varying vec2 vTextureCoordinate;
uniform samplerExternalOES uTexture;
void main() {
gl_FragColor = texture2D(uTexture,vTextureCoordinate);
}
其他的部分
其他的部分和前幾編文章中提到的相差不多俘闯。
0. 生成紋理
這里就是上面所說的。只能用GLES11Ext.GL_TEXTURE_EXTERNAL_OES
這種紋理忽冻。
private int genOesTextureId() {
int[] textureObjectId = new int[1];
GLES20.glGenTextures(1, textureObjectId, 0);
//綁定紋理
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureObjectId[0]);
//設(shè)置放大縮小真朗。設(shè)置邊緣測量
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 textureObjectId[0];
}
1. 視圖矩陣。
因?yàn)樵O(shè)備坐標(biāo)和紋理的坐標(biāo)不同僧诚。而前置攝像頭和后置攝像頭的翻轉(zhuǎn)的方向也不同遮婶。所以要做下面的處理
//計算需要變化的矩陣
private void calculateMatrix() {
//得到通用的顯示的matrix
Gl2Utils.getShowMatrix(mModelMatrix, mPreviewWidth, mPreviewHeight, this.mSurfaceWidth, this.mSurfaceHeight);
if (mCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) { //前置攝像頭
Gl2Utils.flip(mModelMatrix, true, false);
Gl2Utils.rotate(mModelMatrix, 90);
} else { //后置攝像頭
int rotateAngle = 270;
Gl2Utils.rotate(mModelMatrix, rotateAngle);
}
mOesFilter.setMatrix(mModelMatrix);
}
- 得到標(biāo)準(zhǔn)的透視視圖矩陣
public static void getShowMatrix(float[] matrix,int imgWidth,int imgHeight,int viewWidth,int
viewHeight){
if(imgHeight>0&&imgWidth>0&&viewWidth>0&&viewHeight>0){
float sWhView=(float)viewWidth/viewHeight;
float sWhImg=(float)imgWidth/imgHeight;
float[] projection=new float[16];
float[] camera=new float[16];
if(sWhImg>sWhView){
Matrix.orthoM(projection,0,-sWhView/sWhImg,sWhView/sWhImg,-1,1,1,3);
}else{
Matrix.orthoM(projection,0,-1,1,-sWhImg/sWhView,sWhImg/sWhView,1,3);
}
Matrix.setLookAtM(camera,0,0,0,1,0,0,0,0,1,0);
Matrix.multiplyMM(matrix,0,projection,0,camera,0);
}
}
這部分就是標(biāo)準(zhǔn)的處理方式了。誰的比例大湖笨,用誰的旗扑。
- 處理不同攝像頭的旋轉(zhuǎn)
如果是前置攝像頭的話,需要進(jìn)行左右的翻轉(zhuǎn)慈省。然后旋轉(zhuǎn)90度臀防。
后置攝像頭的話,只需要旋轉(zhuǎn)270度就可以了边败。
2. 繪制圖形
重溫一下繪制整體的流程
//draw step
public void draw() {
//step0 clear
onClear();
//step1 use program
onUseProgram();
//step2 active and bind custom data
onSetExpandData();
//step3 bind texture
onBindTexture();
//step4 normal draw
onDraw();
}
onClear
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
這里有一個疑問袱衷。這里其實(shí)不用每次都清除的。只要初始化清除就可以了吧笑窜?
-
onUseProgram
這一步祟昭,就是使用我們之前已經(jīng)創(chuàng)建和link
好的program
private void onUseProgram() {
GLES20.glUseProgram(mProgram);
}
-
onSetExpandData
這里做的其實(shí)就是我們額外給glsl
添加的屬性。應(yīng)用上面我們變化的矩陣怖侦。
private void onSetExpandData() {
GLES20.glUniformMatrix4fv(mUMatrix, 1, false, matrix, 0);
GLES20.glUniformMatrix4fv(mUCoordMatrix, 1, false, mCoordMatrix, 0);
}
-
onBindTexture
接著就是激活和綁定紋理數(shù)據(jù).
private void onBindTexture() {
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + getTextureType());
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, getTextureId());
GLES20.glUniform1i(mUTexture, getTextureType());
}
我們的紋理都掛載在GLES20.GL_TEXTURE0 + getTextureType()
上。同時一定要注意的是谜叹,相機(jī)這里需要的是GLES11Ext.GL_TEXTURE_EXTERNAL_OES
這種拓展類型的紋理采樣匾寝。
-
onDraw
繪制圖像的話,同之前相同荷腊,只需要繪制一個長方形就可以了艳悔。
private void onDraw() {
//設(shè)置定點(diǎn)數(shù)據(jù)
GLES20.glEnableVertexAttribArray(mAPosition);
GLES20.glVertexAttribPointer(
mAPosition,
2,
GLES20.GL_FLOAT,
false,
0,
mVerBuffer);
//
GLES20.glEnableVertexAttribArray(mACoord);
GLES20.glVertexAttribPointer(
mACoord,
2,
GLES20.GL_FLOAT,
false,
0,
mTextureCoordinate);
//繪制三角形帶
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLES20.glDisableVertexAttribArray(mAPosition);
GLES20.glDisableVertexAttribArray(mACoord);
}
GLSurfaceView
最后在GLSurfaceView的對應(yīng)的生命周期內(nèi)調(diào)用方法就可以了~~
錄制
整體流程理解
在原來預(yù)覽的基礎(chǔ)上,我們需要加入MediaCodec
進(jìn)行視頻編碼女仰。
圖中的
EglCore
著保存EglContext
猜年。EglSurface
和EglConfig
的配置抡锈。WindowSurface
就是將EglContext
和Surface
相互關(guān)聯(lián)的幫助類。Encoder
是在EncoderThread
中進(jìn)行乔外。兩個線程(和原來的GLTHread
)需要共享EGLContext
床三。使用
MediaCodec
進(jìn)行視頻編碼,只要通過它的InputSurface
杨幼,將數(shù)據(jù)輸入就可以撇簿。所以通過共享的EGLContext
,來創(chuàng)建一個WindowSurface
差购。然后再通過在該線程內(nèi)GL的draw
方法四瘫,就可以將EGLContext
中的Oes紋理,繪制到Surface
上欲逃。這樣MediaCodec
就可以得到數(shù)據(jù)找蜜。整體流向
當(dāng)我們接受到frame時,我們需要
- 在
GLSurfaceView
的渲染線程,將數(shù)據(jù)渲染到SurfaceView
- 在
Encoder
的線程稳析,將frame
渲染到Mediacodec
的InputSurface
中
Camera==>
SurfaceTexture.onFrameAvailable==>
GLSurfaceView.requestRender ==>
{
//通知更新下一幀
mSurfaceTexture.updateTexImage()
//在`Encoder`的線程洗做,將`frame`渲染到`Mediacodec`的`InputSurface`中。通知編碼器線程繪制并編碼
mVideoEncoder.frameAvailable(mSurfaceTexture) ==>
{
//通知編碼器進(jìn)行編碼
mVideoEncoder.drainEncoder(false);
//刷入數(shù)據(jù)
mFullScreen.drawFrame(mTextureId, transform);
//給InputWindow設(shè)置時間戳
mInputWindowSurface.setPresentationTime(timestampNanos);
//刷新之后迈着,編碼器得到數(shù)據(jù)竭望?
mInputWindowSurface.swapBuffers();
}
//同時Render繪制到屏幕上。在`GLSurfaceView`的渲染線程,將數(shù)據(jù)渲染到`SurfaceView`
mOesFilter.draw();
}
各個部分詳解
TextureMovieEncoder
主要還是添加了一個這個類裕菠。
理想狀態(tài)下咬清,我們創(chuàng)建Video Encoder,然后為它創(chuàng)建EGLContext奴潘,然后將這個context傳入GLSurfaceView來共享旧烧。 但是這里的Api做不到這樣,所以我們只能反著來画髓。當(dāng)GLSurfaceView torn down時掘剪,(可能時我們旋轉(zhuǎn)了屏幕),EGLContext也會同樣被拋棄奈虾。這樣意味這當(dāng)它回來的時候夺谁,我們就需要重新為Video encoder創(chuàng)建EGLContext.(而且,"preserve EGLContext on pause" 這樣的功能肉微,也不啟作用匾鸥。就是上一個暫停狀態(tài)的EGLContext,在這里也不能用)我們可以通過使用TextureView 來替代GLSurfaceView來做一些簡化碉纳。但是這樣會由一點(diǎn)性能的問題勿负。
創(chuàng)建EGL環(huán)境
獲取EGLContext
可以直接在GLThread
中通過EGL14.eglGetCurrentContext()
,就可以得到和線程綁定的EGLContext
(EGLContext
其實(shí)也是存在于ThreadLocal
當(dāng)中)
private void startRecord() {
mOutputFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "camera-test" + System.currentTimeMillis() + ".mp4");
Log.d(TAG, "file path = " + mOutputFile.getAbsolutePath());
// start recording
mVideoEncoder.startRecording(new TextureMovieEncoder.EncoderConfig(
mOutputFile, mPreviewHeight, mPreviewWidth, 1000000, EGL14.eglGetCurrentContext()));
mRecordingStatus = RECORDING_ON;
}
線程的通信是通過Handler
來完成。
public void startRecording(EncoderConfig config) {
Log.d(TAG, "Encoder: startRecording()");
//mReadyFence 這個鎖是來鎖這個線程的所有操作的劳曹。包括開始奴愉。停止琅摩。繪制。
synchronized (mReadyFence) {
if (mRunning) {
Log.w(TAG, "Encoder thread already running");
return;
}
mRunning = true;
new Thread(this, "TextureMovieEncoder").start();
while (!mReady) {
try {
mReadyFence.wait();
} catch (InterruptedException ie) {
// ignore
}
}
}
mHandler.sendMessage(mHandler.obtainMessage(MSG_START_RECORDING, config));
}
創(chuàng)建WindowSurface
锭硼。將EGLContext
和Encoder.InputSurface
關(guān)聯(lián)在一起
private void prepareEncoder(EGLContext sharedContext, int width, int height, int bitRate,
File outputFile) {
try {
//這個就算MediaCodec的封裝房资。包括MediaCodec進(jìn)行編碼。MediaMuxer進(jìn)行視頻封裝
mVideoEncoder = new VideoEncoderCore(width, height, bitRate, outputFile);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
//通過EglContext創(chuàng)建EglCore
mEglCore = new EglCore(sharedContext, EglCore.FLAG_RECORDABLE);
//創(chuàng)建inputWindowSurface
mInputWindowSurface = new WindowSurface(mEglCore, mVideoEncoder.getInputSurface(), true);
//在完成EGL的初始化之后,需要通過eglMakeCurrent()函數(shù)來將當(dāng)前的上下文切換,這樣opengl的函數(shù)才能啟動作用账忘。
mInputWindowSurface.makeCurrent();
mFullScreen = new FullFrameRect(
new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT));
}
這里進(jìn)行了一系列的初始化工作志膀。
- 初始化了
VideoEncoderCore
。它是MediaCodec
和MediaMuxer
的封裝鳖擒。
public VideoEncoderCore(int width, int height, int bitRate, File outputFile)
throws IOException {
//MediaCodec的BufferInfo的緩存溉浙。通過這個BufferInfo不斷的運(yùn)輸數(shù)據(jù)。(原始=>編碼后的)
mBufferInfo = new MediaCodec.BufferInfo();
//創(chuàng)建MediaFormat MIME_TYPE = "video/avc"
MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
//設(shè)置我們想要的參數(shù)蒋荚。如果參數(shù)不合法的話戳稽,在configure時,就會報錯的
//這個ColorFormat很重要期升,這里一定要設(shè)置COLOR_FormatSurface
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
//這里的設(shè)置5 seconds 在相鄰的 I-frames惊奇,why?
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
if (VERBOSE) Log.d(TAG, "format: " + format);
//創(chuàng)建編碼器
mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
//得到對應(yīng)InputSureface
mInputSurface = mEncoder.createInputSurface();
//啟動
mEncoder.start();
//創(chuàng)建MediaMuxer。我們不能直接在這里開始muxer.因?yàn)镸ediaFormat 還沒得到輸入播赁。必須要在編碼器得到輸入之后颂郎,才能添加。這里先不添加音頻容为。
mMuxer = new MediaMuxer(outputFile.toString(),
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
mTrackIndex = -1;
mMuxerStarted = false;
}
- 初始化了
EglCore
乓序。主要是管理EGL的state,包括 (display, context, config)坎背。
//主要是初始化display 和EglContext
public EglCore(EGLContext sharedContext, int flags) {
if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
throw new RuntimeException("EGL already set up");
}
if (sharedContext == null) {
sharedContext = EGL14.EGL_NO_CONTEXT;
}
//先創(chuàng)建一個默認(rèn)的Display
mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
throw new RuntimeException("unable to get EGL14 display");
}
//檢查是否創(chuàng)建成功
int[] version = new int[2];
if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
mEGLDisplay = null;
throw new RuntimeException("unable to initialize EGL14");
}
// Try to get a GLES3 context, if requested.
if ((flags & FLAG_TRY_GLES3) != 0) {
//Log.d(TAG, "Trying GLES 3");
EGLConfig config = getConfig(flags, 3);
if (config != null) {
int[] attrib3_list = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,
EGL14.EGL_NONE
};
EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext,
attrib3_list, 0);
if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) {
//Log.d(TAG, "Got GLES 3 config");
mEGLConfig = config;
mEGLContext = context;
mGlVersion = 3;
}
}
}
if (mEGLContext == EGL14.EGL_NO_CONTEXT) { // GLES 2 only, or GLES 3 attempt failed
//Log.d(TAG, "Trying GLES 2");
//獲取GL的Config
EGLConfig config = getConfig(flags, 2);
if (config == null) {
throw new RuntimeException("Unable to find a suitable EGLConfig");
}
int[] attrib2_list = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
EGL14.EGL_NONE
};
//獲取EGLContext關(guān)鍵方法就是它替劈,EGL14.eglCreateContext
EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext,
attrib2_list, 0);
checkEglError("eglCreateContext");
mEGLConfig = config;
mEGLContext = context;
mGlVersion = 2;
}
// Confirm with query.
int[] values = new int[1];
EGL14.eglQueryContext(mEGLDisplay, mEGLContext, EGL14.EGL_CONTEXT_CLIENT_VERSION,
values, 0);
Log.d(TAG, "EGLContext created, client version " + values[0]);
}
//EGL的配置。類似鍵值對的數(shù)組得滤。
private EGLConfig getConfig(int flags, int version) {
int renderableType = EGL14.EGL_OPENGL_ES2_BIT;
if (version >= 3) {
renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR;
}
// The actual surface is generally RGBA or RGBX, so situationally omitting alpha
// doesn't really help. It can also lead to a huge performance hit on glReadPixels()
// when reading into a GL_RGBA buffer.
int[] attribList = {
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8,
//EGL14.EGL_DEPTH_SIZE, 16,
//EGL14.EGL_STENCIL_SIZE, 8,
EGL14.EGL_RENDERABLE_TYPE, renderableType,
EGL14.EGL_NONE, 0, // placeholder for recordable [@-3]
EGL14.EGL_NONE
};
if ((flags & FLAG_RECORDABLE) != 0) {
attribList[attribList.length - 3] = EGL_RECORDABLE_ANDROID;
attribList[attribList.length - 2] = 1;
}
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length,
numConfigs, 0)) {
Log.w(TAG, "unable to find RGB8888 / " + version + " EGLConfig");
return null;
}
return configs[0];
}
- 初始化了
WindowSurface
陨献。并通過eglMakeCurrent()函數(shù),切換到當(dāng)前的上下文懂更。
/**
* 將EGL和原生的window surface關(guān)聯(lián)在一起
* 如果傳入releaseSurface為true的話眨业,當(dāng)你調(diào)用release方法時,這個Surface就會自動被release沮协。
* 但時如果是使用了SurfaceView的Surface等坛猪,Android框架創(chuàng)建的Surface時需要注意,
* 它會干涉原生框架的調(diào)用皂股,比如上述的SurfaceView的Surface,release之后,surfaceDestroyed()回調(diào)將不會再收到
*/
public WindowSurface(EglCore eglCore, Surface surface, boolean releaseSurface) {
super(eglCore);
createWindowSurface(surface);
mSurface = surface;
mReleaseSurface = releaseSurface;
}
/**
* 創(chuàng)建 window surface.我們的之前的信息提前就保存再EglCore內(nèi)了
* <p>
* @param surface May be a Surface or SurfaceTexture.
*/
public void createWindowSurface(Object surface) {
if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
throw new IllegalStateException("surface already created");
}
mEGLSurface = mEglCore.createWindowSurface(surface);
}
/**
* 如果我們是為了MediaCodec創(chuàng)建命黔,那么EGLConfig需要有"recordable"的attribute.這個部分呜呐,在上面初始化EglCore時就斤,已經(jīng)完成了EGLConfig和EGLDisplay的配置
*/
public EGLSurface createWindowSurface(Object surface) {
if (!(surface instanceof Surface) && !(surface instanceof SurfaceTexture)) {
throw new RuntimeException("invalid surface: " + surface);
}
// Create a window surface, and attach it to the Surface we received.
int[] surfaceAttribs = {
EGL14.EGL_NONE
};
//這就是我們想要的EGLSurface的創(chuàng)建方式 EGL14.eglCreateWindowSurface
EGLSurface eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surface,
surfaceAttribs, 0);
checkEglError("eglCreateWindowSurface");
if (eglSurface == null) {
throw new RuntimeException("surface was null");
}
return eglSurface;
}
- 初始化
FullFrameRect
。它是OpenGl繪制命令等的封裝蘑辑。
frameAvailable
在接受到Frame
時洋机,進(jìn)行編碼
mVideoEncoder.frameAvailable(mSurfaceTexture);
- 時間戳和tranfrom矩陣
float[] transform = new float[16]; // TODO - avoid alloc every frame
st.getTransformMatrix(transform);
long timestamp = st.getTimestamp();
if (timestamp == 0) {
// Seeing this after device is toggled off/on with power button. The
// first frame back has a zero timestamp.
//
// MPEG4Writer thinks this is cause to abort() in native code, so it's very
// important that we just ignore the frame.
Log.w(TAG, "HEY: got SurfaceTexture with timestamp of zero");
return;
}
mHandler.sendMessage(mHandler.obtainMessage(MSG_FRAME_AVAILABLE,
(int) (timestamp >> 32), (int) timestamp, transform));
- 編碼
private void handleFrameAvailable(float[] transform, long timestampNanos) {
if (VERBOSE) Log.d(TAG, "handleFrameAvailable tr=" + transform);
//視頻編碼
mVideoEncoder.drainEncoder(false);
mFullScreen.drawFrame(mTextureId, transform);
//設(shè)置時間戳
mInputWindowSurface.setPresentationTime(timestampNanos);
mInputWindowSurface.swapBuffers();
}
/**
* 從encoder中得到數(shù)據(jù),再寫入到muxer中洋魂。
* 下面這段代碼就是通用的編碼的代碼了
*/
public void drainEncoder(boolean endOfStream) {
final int TIMEOUT_USEC = 10000;
if (VERBOSE) Log.d(TAG, "drainEncoder(" + endOfStream + ")");
//如果通知編碼器結(jié)束绷旗,就會signalEndOfInputStream
if (endOfStream) {
if (VERBOSE) Log.d(TAG, "sending EOS to encoder");
mEncoder.signalEndOfInputStream();
}
//得到outputBuffer
ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers();
//不斷循環(huán),當(dāng)讀取所有數(shù)據(jù)時
while (true) {
//上面換成的BufferInfo副砍,送入到Encoder中衔肢,去查詢狀態(tài)
int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
//如果時繼續(xù)等待,就暫時不用處理豁翎。大多數(shù)情況角骤,都是從這兒跳出循環(huán)
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
// no output available yet
if (!endOfStream) {
break; // out of while
} else {
if (VERBOSE) Log.d(TAG, "no output available, spinning to await EOS");
}
//outputBuffer發(fā)生變化了。就重新去獲取
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
// not expected for an encoder
encoderOutputBuffers = mEncoder.getOutputBuffers();
//格式發(fā)生變化心剥。這個第一次configure之后也會調(diào)用一次邦尊。在這里進(jìn)行muxer的初始化
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// should happen before receiving buffers, and should only happen once
if (mMuxerStarted) {
throw new RuntimeException("format changed twice");
}
MediaFormat newFormat = mEncoder.getOutputFormat();
Log.d(TAG, "encoder output format changed: " + newFormat);
// now that we have the Magic Goodies, start the muxer
mTrackIndex = mMuxer.addTrack(newFormat);
mMuxer.start();
mMuxerStarted = true;
} else if (encoderStatus < 0) {
Log.w(TAG, "unexpected result from encoder.dequeueOutputBuffer: " +
encoderStatus);
// let's ignore it
} else {
//寫入數(shù)據(jù)
ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
if (encodedData == null) {
throw new RuntimeException("encoderOutputBuffer " + encoderStatus +
" was null");
}
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
// The codec config data was pulled out and fed to the muxer when we got
// the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it.
if (VERBOSE) Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
mBufferInfo.size = 0;
}
if (mBufferInfo.size != 0) {
if (!mMuxerStarted) {
throw new RuntimeException("muxer hasn't started");
}
//切到對應(yīng)的位置,進(jìn)行書寫
// adjust the ByteBuffer values to match BufferInfo (not needed?)
encodedData.position(mBufferInfo.offset);
encodedData.limit(mBufferInfo.offset + mBufferInfo.size);
//寫入
mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
if (VERBOSE) {
Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer, ts=" +
mBufferInfo.presentationTimeUs);
}
}
//重新釋放优烧,為了下一次的輸入
mEncoder.releaseOutputBuffer(encoderStatus, false);
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
//到達(dá)最后了蝉揍,就跳出循環(huán)
if (!endOfStream) {
Log.w(TAG, "reached end of stream unexpectedly");
} else {
if (VERBOSE) Log.d(TAG, "end of stream reached");
}
break; // out of while
}
}
}
}
添加濾鏡
整體流程理解
上面,我們是直接繪制OES的紋理畦娄。這里又沾,因?yàn)橐砑訛V鏡的效果。所以我們需要將紋理進(jìn)行處理纷责。
離屏繪制
先將
OES
紋理捍掺,綁定到FrameBuffer
上。同時會在FrameBuffer
上綁定一個新的textureId
(這里命名為OffscreenTextureId
)再膳。然后調(diào)用繪制OES
紋理的方法挺勿,數(shù)據(jù)就會傳遞到FBO
上。而我們可以通過綁定在其上的OffscreenTextureId
得到其數(shù)據(jù)喂柒。通常情況下不瓶,我們把綁定FrameBuffer
和繪制這個新的OffscreenTextureId
代表的紋理的過程,稱為離屏繪制灾杰。
綁定和生成FrameBuffer
的時機(jī)
創(chuàng)建FrameBuffer
蚊丐。因?yàn)镽enderBuffer的存儲大小要和當(dāng)前的顯示的寬和高相關(guān)。所以會在onSurfaceChanged
生命周期方法時候調(diào)用艳吠。
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
//在這里監(jiān)聽到尺寸的改變麦备。做出對應(yīng)的變化
prepareFramebuffer(width, height);
//...
}
//生成frameBuffer的時機(jī)
private void prepareFramebuffer(int width, int height) {
int[] values = new int[1];
//申請一個與FrameBuffer綁定的textureId
GLES20.glGenTextures(1, values, 0);
mOffscreenTextureId = values[0];
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mOffscreenTextureId);
// Create texture storage.
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0,
GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
// Set parameters. We're probably using non-power-of-two dimensions, so
// some values may not be available for use.
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,
GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,
GLES20.GL_CLAMP_TO_EDGE);
//創(chuàng)建FrameBuffer Object并且綁定它
GLES20.glGenFramebuffers(1, values, 0);
mFrameBuffer = values[0];
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffer);
// 創(chuàng)建RenderBuffer Object并且綁定它
GLES20.glGenRenderbuffers(1, values, 0);
mRenderBuffer = values[0];
GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, mRenderBuffer);
//為我們的RenderBuffer申請存儲空間
GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, width, height);
// 將renderBuffer掛載到frameBuffer的depth attachment 上。就上面申請了OffScreenId和FrameBuffer相關(guān)聯(lián)
GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT, GLES20.GL_RENDERBUFFER, mRenderBuffer);
// 將text2d掛載到frameBuffer的color attachment上
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, mOffscreenTextureId, 0);
// See if GLES is happy with all this.
int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) {
throw new RuntimeException("Framebuffer not complete, status=" + status);
}
// 先不使用FrameBuffer,將其切換掉凛篙。到開始繪制的時候黍匾,在綁定回來
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
}
//在onDrawFrame中添加代碼
@Override
public void onDrawFrame(GL10 gl) {
//...省略
//重新切換到FrameBuffer上。
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffer);
//這里的繪制呛梆,就會將數(shù)據(jù)掛載到FrameBuffer上了锐涯。
mOesFilter.draw();
//解除綁定,結(jié)束FrameBuffer部分的數(shù)據(jù)寫入
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
//....省略
}
-
FrameBuffer
幀緩沖對象
openGL繪制流程.png
我們自己創(chuàng)建的FrameBuffer其實(shí)只是一個容器填物。所以我們要將數(shù)據(jù)掛載上去纹腌,它才算是完整。
所以滞磺,我們可以看到申請F(tuán)rameBuffer需要進(jìn)行下面的三步
- 生成一個FrameBuffer
- 申請一個RenderBuffer,并且掛載
GL_DEPTH_ATTACHMENT
上升薯。
RenderBuffer
也是一個渲染緩沖區(qū)對象。RenderBuffer
對象是新引入的用于離屏渲染雁刷。它允許將場景直接渲染到Renderbuffer
對象覆劈,而不是渲染到紋理對象。
Renderbuffe
r只是一個包含可渲染內(nèi)部格式的單個映像的數(shù)據(jù)存儲對象沛励。它用于存儲沒有相應(yīng)紋理格式的OpenGL
邏輯緩沖區(qū)责语,如模板或深度緩沖區(qū)。
- 申請一個
textureId
,掛載到GL_COLOR_ATTACHMENT0
上目派。 - 重新切換到FrameBuffer上(綁定)坤候,然后繪制。
我們就可以通過這個紋理企蹭,得到保存在FBO
上的數(shù)據(jù)了
添加濾鏡的繪制
我們可以通過FBO
白筹,進(jìn)行濾鏡處理。我們將得到的數(shù)據(jù)谅摄,再次進(jìn)行繪制徒河,在這次的繪制中,我們就可以添加上我們想要的濾鏡處理了送漠。
但是這里不僅僅是要繪制到屏幕上顽照,同時要在開啟錄制的時候,輸入給Encoder進(jìn)行視頻的編碼和封裝闽寡。
所以我們需要將數(shù)據(jù)再寫寫入一個新的FrameBuffer中聪廉,然后再其輸出的outputTexture中中姜,就可以得到應(yīng)用了紋理的數(shù)據(jù)了。
- 濾鏡處理
就算將上面的OffscreenTextureId作為這里濾鏡的輸入Id.
@Override
public void onDrawFrame(GL10 gl) {
//...省略
//經(jīng)過路徑處理
mColorFilter.setTextureId(mOffscreenTextureId);
mColorFilter.onDrawFrame();
int outputTextureId = mColorFilter.getOutputTextureId();
//...省略
}
同時濾鏡內(nèi)肢扯,也按照上面的FrameBuffer
的處理流程喘沿。將數(shù)據(jù)掛載到FrameBuffer
上吁脱。得到掛載在FrameBuffer
上的outputTextureId
代碼同上涯保,省略
將應(yīng)用了濾鏡的紋理分別繪制到GLView
和Encoder
當(dāng)中
@Override
public void onDrawFrame(GL10 gl) {
//省略...
//經(jīng)過濾鏡處理
mColorFilter.setTextureId(mOffscreenTextureId);
mColorFilter.onDrawFrame();
int outputTextureId = mColorFilter.getOutputTextureId();
//將得到的outputTextureId一死,輸入encoder,進(jìn)行編碼
mVideoEncoder.setTextureId(outputTextureId);
mVideoEncoder.frameAvailable(mSurfaceTexture);
//將得到的outputTextureId,再次Draw,因?yàn)闆]有FrameBuffer,所以這次Draw的數(shù)據(jù)鹿响,就直接到了Surface上了。
mShowFilter.setTextureId(outputTextureId);
mShowFilter.onDrawFrame();
}
- 將得到的
outputTextureId
支救,輸入Encoder
的InputSurface
中,通知內(nèi)部進(jìn)行draw
和進(jìn)行編碼抢野。 - 將得到的
outputTextureId
,再次Draw,因?yàn)闆]有FrameBuffer
,所以這次draw
的數(shù)據(jù),就直接到了Surface
上了各墨。也就是直接繪制到了我們的GLSurfaceView
上了。
小結(jié)
- 對比之前繪制流程启涯。上文是直接將紋理繪制到了
GLView
上顯示贬堵,而這里是將紋理繪制到綁定的FrameBuffer
中,而且
繪制的結(jié)果不直接顯示出來结洼。所以可以形象的理解離屏繪制黎做,就是將繪制的結(jié)果保存在與FrameBuffer
綁定的一個新的textureId
(OffscreenTextureId
)中,不直接繪制到屏幕上松忍。 - 把握好整體流程之后蒸殿,這部分的處理也會變得簡單起來。后面就可以如何添加更加炫酷的濾鏡和玩法了鸣峭。
最后
整編文章就重要的部分還是在理解整個紋理中數(shù)據(jù)傳遞的路線宏所。
后續(xù),會對這里的相機(jī)的預(yù)覽添加其他的濾鏡摊溶。
Demo位置
https://github.com/deepsadness/ZeroToOpenGL
系列文章地址
Android OpenGL ES(一)-開始描繪一個平面三角形
Android OpenGL ES(二)-正交投影
Android OpenGL ES(三)-平面圖形
Android OpenGL ES(四)-為平面圖添加濾鏡
Android OpenGL ES(五)-結(jié)合相機(jī)進(jìn)行預(yù)覽/錄制及添加濾鏡
Android OpenGL ES(六) - 將輸入源換成視頻
Android OpenGL ES(七) - 生成抖音照片電影