
之前我們結(jié)合相機和視頻,結(jié)合濾鏡,做了實時的預(yù)覽和錄制。
這期,我們來試試利用OpenGL+MediaCodc,不進行預(yù)覽直接錄制成視頻的情況。
兩個問題
錄制視頻的開始,我們先來思考兩個問題:
- 如何直接生成影片。(不同于之前邊預(yù)覽邊錄制的流程)
- 如何確定影片的幀數(shù)。(不同于之前,都是通過Api通知,完成幀之后的回調(diào))
直接生成影片
OpenGL繪制
通過之前的學習,我們通過閱讀源碼和文章,能夠了解到整個OpenGL繪制的流程時這樣的。

之前文章中寫到的這些部分,都是直接由GLSurfaceView幫我們完成了。
預(yù)覽部分 - 手機屏幕上顯示
之前的預(yù)覽部分都是直接使用GLSurfaceView。
因為GLSurfaceView已經(jīng)為我們當前的線程準備好了EGL的環(huán)境。所以我們只要生成自己的紋理texture,并進行繪制就可以了。
繪制的結(jié)果,就會出現(xiàn)在準備好的EGLSurface當中。
那GLSurfaceView的EGLSurface是怎么關(guān)聯(lián)的呢?
- 繼承
通過閱讀源碼可以看到,GLSurfaceView直接繼承了SurfaceView
繼承SurfaceView.png - 創(chuàng)建
同時,通過mSurfaceHolder來創(chuàng)建EGLSurface
創(chuàng)建ElgSurface.png
這樣,使用draw之后,通過eglSwapBuffers,就會將內(nèi)容繪制到GLSurfaceView當中。
錄制部分
通過預(yù)覽部分的回顧,我們知道,通過用SurfaceView進行創(chuàng)建和關(guān)聯(lián)EGLSurface,就可以繪制到整個SurfaceView上。er實際上,錄制就是同時輸入到了Encoder的Surface當中了。
-
那我們這兒又多了一個想要繪制的Surface要怎么辦呢?
我們知道,繪制實際上是將緩存在紋理上的進行,進行輸出。而紋理是和線程中的EglContext綁定。
所以,我們只要能得到這個結(jié)果的紋理,保持相同的EglContext,重新繪制一次,就有相同的結(jié)果了。
這樣我們就可以利用Encoder的InputSurface和相同的EglContext,來再次創(chuàng)建一個EglSurface。在這里繪制相同的紋理,就可以得到相同的結(jié)果。
//1 . 創(chuàng)建
//得到當前線程的EGLContext
EGL14.eglGetCurrentContext();
//在新的線程中,進行創(chuàng)建新的 EGLSurface
mEglCore = new EglCore(sharedContext, EglCore.FLAG_RECORDABLE);
mInputWindowSurface = new WindowSurface(mEglCore, mVideoEncoder.getInputSurface(), true);
mInputWindowSurface.makeCurrent();
//2. 繪制
mFullScreen.drawFrame(mTextureId, transform);
mInputWindowSurface.setPresentationTime(timestampNanos);
mInputWindowSurface.swapBuffers();
對比
對比,我們就能發(fā)現(xiàn)。
- 要在屏幕上顯示,需要使用
SurfaceView或其他Android原生的View來創(chuàng)建對應(yīng)的EGLSurface - 利用
Encoder進行錄制,我們只需要利用它的InputSurface來創(chuàng)建,EGLSurface就可以了。
這里有個問題。如果我們想要使用FFmpeg,并且不使用Camera的回調(diào)來接受數(shù)據(jù)的話,要怎么辦呢?
確定影片的幀數(shù)(繪制的時機)
通常的影片的幀數(shù)(fps)都是30。所以我們只要保持編碼時,輸入的時間戳是相隔30fps就可以完成這樣。
//fps 30
private long computePresentationTimeNsec(int frameIndex) {
final long ONE_BILLION = 1000000000;
return frameIndex * ONE_BILLION / 30;
}
整體
整個流程需要異步。和UI回調(diào)
直接使用了HandlerThread。和使用MainLooper來創(chuàng)建Handler就可以完成。
這里需要注意的是,進行線程通信時,要確保內(nèi)部的Handler已經(jīng)創(chuàng)建,需要進行getLooper()之后,來創(chuàng)建Handler.
這里的getLooper()是一個同步的方法,只要當前的Thread不是結(jié)束的狀態(tài),就能確保得到非空的Looper.
private MovieHandler getMovieHandler() {
if (mMovieHandler == null) {
mMovieHandler = new MovieHandler(getLooper(), this);
}
return mMovieHandler;
}
模仿Render,將繪制的流程解耦出來
這樣就可以自由的進行繪制。
同時我們需要Duration的屬性,這樣我們能在正確的時間范圍內(nèi),取到我們想要的Render和讓Render針對時間進行變形。
繪制的方法,同時加上當前的時間戳
public interface MovieMaker {
long ONE_BILLION = 1000000000;
void onGLCreate();
void setSize(int width, int height);
long getDurationAsNano();
void generateFrame(long curTime);
void release();
}
整體的繪制流程
private void makeMovie() {
//不斷繪制。
boolean isCompleted = false;
try {
//初始化GL環(huán)境
mEglCore = new EglCore(null, EglCore.FLAG_RECORDABLE);
mVideoEncoder = new VideoEncoderCore(width, height, bitRate, outputFile);
Surface encoderInputSurface = mVideoEncoder.getInputSurface();
mWindowSurface = new WindowSurface(mEglCore, encoderInputSurface, true);
mWindowSurface.makeCurrent();
//繪制
// 計算時長
long totalDuration = 0;
timeSections = new long[movieMakers.size()];
for (int i = 0; i < movieMakers.size(); i++) {
MovieMaker movieMaker = movieMakers.get(i);
movieMaker.onGLCreate();
movieMaker.setSize(width, height);
timeSections[i] = totalDuration;
totalDuration += movieMaker.getDurationAsNano();
}
if (listener != null) {
uiHandler.post(() -> {
listener.onStart();
});
}
long tempTime = 0;
int frameIndex = 0;
while (tempTime <= totalDuration) {
mVideoEncoder.drainEncoder(false);
generateFrame(tempTime);
long presentationTimeNsec = computePresentationTimeNsec(frameIndex);
submitFrame(presentationTimeNsec);
updateProgress(tempTime, totalDuration);
frameIndex++;
tempTime = presentationTimeNsec;
if (stop) {
break;
}
}
//finish
mVideoEncoder.drainEncoder(true);
isCompleted = true;
} catch (Exception e) {
e.printStackTrace();
} finally {
//結(jié)束
try {
releaseEncoder();
} catch (Exception e) {
e.printStackTrace();
}
if (isCompleted && listener != null) {
uiHandler.post(() -> {
listener.onCompleted(outputFile.getAbsolutePath());
});
}
}
}
同樣是先創(chuàng)建對應(yīng)的EGL環(huán)境。然后在給定的時長下,調(diào)用對應(yīng)的Render進行繪制。
應(yīng)用
簡單的靜態(tài)圖片的展示
- 創(chuàng)建
MovieMaker
就是使用之前創(chuàng)建好的Render在對應(yīng)的生命周期方法調(diào)用。因為是靜態(tài)圖片。所以這里沒有進行變化。
public class StaticPhotoMaker implements MovieMaker {
PhotoFilter photoFilter;
String filePath;
public StaticPhotoMaker(String filePath) {
this.filePath = filePath;
}
@Override
public void onGLCreate() {
photoFilter = new PhotoFilter();
photoFilter.onCreate();
}
@Override
public void setSize(int width, int height) {
photoFilter.onSizeChange(width, height);
Bitmap bitmap = BitmapFactory.decodeFile(filePath);
photoFilter.setBitmap(bitmap);
}
@Override
public long getDurationAsNano() {
return 3 * ONE_BILLION;
}
@Override
public void generateFrame(long curTime) {
photoFilter.onDrawFrame();
}
@Override
public void release() {
photoFilter.release();
}
}
- 調(diào)用
@SuppressLint("StaticFieldLeak")
public void startGenerate(View view) {
engine = new MovieEngine.MovieBuilder()
.maker(new TestMaker("/storage/emulated/0/tencent/MicroMsg/WeiXin/mmexport1529734446397.png"))
.maker(new TestMaker("/storage/emulated/0/tencent/MicroMsg/WeiXin/mmexport1529911150337.png"))
.maker(new TestMaker("/storage/emulated/0/tencent/MicroMsg/WeiXin/mmexport1531208871527.png"))
.width(720)
.height(1280)
.listener(new MovieEngine.ProgressListener() {
private long startTime;
@Override
public void onStart() {
startTime = System.currentTimeMillis();
Toast.makeText(GenerateMovieActivity.this, "onStart!!", Toast.LENGTH_SHORT).show();
}
@Override
public void onCompleted(String absolutePath) {
long endTime = System.currentTimeMillis();
Toast.makeText(GenerateMovieActivity.this, "file path=" + absolutePath + ",cost time = " + (endTime - startTime), Toast.LENGTH_SHORT).show();
}
@Override
public void onProgress(long current, long totalDuration) {
String text = "當前進度是" + (current * 1f / totalDuration * 1f);
textView.setText(text);
}
}).build();
engine.make();
}
- 結(jié)果
每三秒切換靜態(tài)圖片。

添加類似抖音的動態(tài)變化
因為動畫效果,需要同時對兩圖進行效果。所以需要兩個不同的Render進行變化。
- 定義動態(tài)的
MovieMaker
- 構(gòu)造方法
public AnimateGroupPhotoMaker(String... filePaths) {
this.filePaths = new ArrayList<>();
this.filePaths.addAll(Arrays.asList(filePaths));
}
- 做矩陣變化完成,動畫
因為我們已經(jīng)預(yù)留好了傳入時間的變化,所以只要根據(jù)這個時間變化,進行變化矩陣就可以了。
@Override
public void generateFrame(long curTime) {
if (curTime == 0) {
startTime = curTime;
}
float dif = (curTime - startTime) * 1f / getDurationAsNano();
for (int i = 0; i < photoFilters.size(); i++) {
PhotoAlphaFilter2 photoFilter = photoFilters.get(i);
transform(photoFilter, dif, i);
photoFilter.onDrawFrame();
}
}
//進行動畫的變化
private void transform(PhotoAlphaFilter2 photoFilter, float dif, int i) {
System.out.println("dif = " + dif);
if (srcMatrix == null) {
srcMatrix = photoFilter.getMVPMatrix();
}
float[] mModelMatrix = Arrays.copyOf(srcMatrix, 16);
float v;
switch (i) {
//第一個做縮小的動畫
case 0:
v = 1f - dif * 0.1f;
Matrix.scaleM(mModelMatrix, 0, v, v, 0f);
photoFilter.setAlpha(1 - dif * 0.5f);
break;
//第二個做平移的動畫
case 1:
v = 2 - dif * 2f;
int offset = (int) (width * (v / 2));
System.out.println("translateM v = " + v);
Matrix.translateM(mModelMatrix, 0, v, 0f, 0f);
break;
}
photoFilter.setMVPMatrix(mModelMatrix);
}
- 使用
@SuppressLint("StaticFieldLeak")
public void startGenerate(View view) {
engine = new MovieEngine.MovieBuilder()
//結(jié)合原來靜態(tài)的圖片顯示。組成幻燈片的效果
.maker(new StaticPhotoMaker("/storage/emulated/0/tencent/MicroMsg/WeiXin/mmexport1529734446397.png"))
.maker(new AnimateGroupPhotoMaker("/storage/emulated/0/tencent/MicroMsg/WeiXin/mmexport1529734446397.png", "/storage/emulated/0/tencent/MicroMsg/WeiXin/mmexport1531208871527.png"))
.maker(new StaticPhotoMaker("/storage/emulated/0/tencent/MicroMsg/WeiXin/mmexport1531208871527.png"))
.maker(new AnimateGroupPhotoMaker("/storage/emulated/0/tencent/MicroMsg/WeiXin/mmexport1531208871527.png", "/storage/emulated/0/tencent/MicroMsg/WeiXin/mmexport1529911150337.png"))
.maker(new StaticPhotoMaker("/storage/emulated/0/tencent/MicroMsg/WeiXin/mmexport1529911150337.png"))
.width(720)
.height(1280)
.listener(new MovieEngine.ProgressListener() {
private ProgressDialog progressDialog;
private long startTime;
@Override
public void onStart() {
startTime = System.currentTimeMillis();
Toast.makeText(GenerateMovieActivity.this, "onStart!!", Toast.LENGTH_SHORT).show();
progressDialog = new ProgressDialog(GenerateMovieActivity.this);
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.show();
progressDialog.setMax(100);
}
@Override
public void onCompleted(String absolutePath) {
progressDialog.hide();
long endTime = System.currentTimeMillis();
Toast.makeText(GenerateMovieActivity.this, "file path=" + absolutePath + ",cost time = " + (endTime - startTime), Toast.LENGTH_SHORT).show();
}
@Override
public void onProgress(long current, long totalDuration) {
float progress = current * 1f / totalDuration * 1f;
progressDialog.setProgress((int) (progress * 100));
}
}).build();
engine.make();
}
-
結(jié)果
每三秒靜態(tài)圖片和0.35s動畫切換。
movie-ge-2.gif
源碼
文中Demo源碼的github地址
系列文章地址
Android OpenGL ES(一)-開始描繪一個平面三角形
Android OpenGL ES(二)-正交投影
Android OpenGL ES(三)-平面圖形
Android OpenGL ES(四)-為平面圖添加濾鏡
Android OpenGL ES(五)-結(jié)合相機進行預(yù)覽/錄制及添加濾鏡
Android OpenGL ES(六) - 將輸入源換成視頻
Android OpenGL ES(七) - 生成抖音照片電影


