之前我們結(jié)合相機(jī)和視頻,結(jié)合濾鏡砚著,做了實(shí)時(shí)的預(yù)覽和錄制次伶。
這期,我們來(lái)試試?yán)?code>OpenGL+MediaCodc
稽穆,不進(jìn)行預(yù)覽直接錄制成視頻的情況冠王。
兩個(gè)問(wèn)題
錄制視頻的開(kāi)始,我們先來(lái)思考兩個(gè)問(wèn)題:
- 如何直接生成影片舌镶。(不同于之前邊預(yù)覽邊錄制的流程)
- 如何確定影片的幀數(shù)柱彻。(不同于之前豪娜,都是通過(guò)Api通知,完成幀之后的回調(diào))
直接生成影片
OpenGL
繪制
通過(guò)之前的學(xué)習(xí)绒疗,我們通過(guò)閱讀源碼和文章侵歇,能夠了解到整個(gè)OpenGL
繪制的流程時(shí)這樣的。
之前文章中寫到的這些部分吓蘑,都是直接由GLSurfaceView
幫我們完成了惕虑。
預(yù)覽部分 - 手機(jī)屏幕上顯示
之前的預(yù)覽部分都是直接使用GLSurfaceView
。
因?yàn)?code>GLSurfaceView已經(jīng)為我們當(dāng)前的線程準(zhǔn)備好了EGL
的環(huán)境磨镶。所以我們只要生成自己的紋理texture
,并進(jìn)行繪制就可以了溃蔫。
繪制的結(jié)果,就會(huì)出現(xiàn)在準(zhǔn)備好的EGLSurface
當(dāng)中琳猫。
那GLSurfaceView
的EGLSurface
是怎么關(guān)聯(lián)的呢伟叛?
- 繼承
通過(guò)閱讀源碼可以看到,GLSurfaceView
直接繼承了SurfaceView
- 創(chuàng)建
同時(shí),通過(guò)mSurfaceHolder
來(lái)創(chuàng)建EGLSurface
這樣脐嫂,使用draw
之后统刮,通過(guò)eglSwapBuffers
,就會(huì)將內(nèi)容繪制到GLSurfaceView
當(dāng)中。
錄制部分
通過(guò)預(yù)覽部分的回顧账千,我們知道侥蒙,通過(guò)用SurfaceView
進(jìn)行創(chuàng)建和關(guān)聯(lián)EGLSurface
,就可以繪制到整個(gè)SurfaceView
上。er實(shí)際上匀奏,錄制就是同時(shí)輸入到了Encoder
的Surface
當(dāng)中了鞭衩。
-
那我們這兒又多了一個(gè)想要繪制的Surface要怎么辦呢?
我們知道娃善,繪制實(shí)際上是將緩存在紋理上的進(jìn)行论衍,進(jìn)行輸出。而紋理是和線程中的EglContext
綁定聚磺。
所以坯台,我們只要能得到這個(gè)結(jié)果的紋理,保持相同的EglContext
瘫寝,重新繪制一次蜒蕾,就有相同的結(jié)果了。
這樣我們就可以利用Encoder
的InputSurface
和相同的EglContext
,來(lái)再次創(chuàng)建一個(gè)EglSurface
矢沿。在這里繪制相同的紋理滥搭,就可以得到相同的結(jié)果酸纲。
//1 . 創(chuàng)建
//得到當(dāng)前線程的EGLContext
EGL14.eglGetCurrentContext();
//在新的線程中捣鲸,進(jìn)行創(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();
對(duì)比
對(duì)比,我們就能發(fā)現(xiàn)闽坡。
- 要在屏幕上顯示栽惶,需要使用
SurfaceView
或其他Android
原生的View
來(lái)創(chuàng)建對(duì)應(yīng)的EGLSurface - 利用
Encoder
進(jìn)行錄制愁溜,我們只需要利用它的InputSurface
來(lái)創(chuàng)建,EGLSurface
就可以了外厂。
這里有個(gè)問(wèn)題冕象。如果我們想要使用FFmpeg
,并且不使用Camera
的回調(diào)來(lái)接受數(shù)據(jù)的話汁蝶,要怎么辦呢渐扮?
確定影片的幀數(shù)(繪制的時(shí)機(jī))
通常的影片的幀數(shù)(fps)都是30。所以我們只要保持編碼時(shí)掖棉,輸入的時(shí)間戳是相隔30fps就可以完成這樣墓律。
//fps 30
private long computePresentationTimeNsec(int frameIndex) {
final long ONE_BILLION = 1000000000;
return frameIndex * ONE_BILLION / 30;
}
整體
整個(gè)流程需要異步。和UI回調(diào)
直接使用了HandlerThread
幔亥。和使用MainLooper
來(lái)創(chuàng)建Handler
就可以完成耻讽。
這里需要注意的是,進(jìn)行線程通信時(shí)帕棉,要確保內(nèi)部的Handler已經(jīng)創(chuàng)建针肥,需要進(jìn)行getLooper()
之后,來(lái)創(chuàng)建Handler
.
這里的getLooper()
是一個(gè)同步的方法香伴,只要當(dāng)前的Thread不是結(jié)束的狀態(tài)慰枕,就能確保得到非空的Looper
.
private MovieHandler getMovieHandler() {
if (mMovieHandler == null) {
mMovieHandler = new MovieHandler(getLooper(), this);
}
return mMovieHandler;
}
模仿Render
,將繪制的流程解耦出來(lái)
這樣就可以自由的進(jìn)行繪制。
同時(shí)我們需要Duration
的屬性瞒窒,這樣我們能在正確的時(shí)間范圍內(nèi)捺僻,取到我們想要的Render和讓Render針對(duì)時(shí)間進(jìn)行變形。
繪制的方法崇裁,同時(shí)加上當(dāng)前的時(shí)間戳
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();
//繪制
// 計(jì)算時(shí)長(zhǎng)
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)建對(duì)應(yīng)的EGL
環(huán)境。然后在給定的時(shí)長(zhǎng)下拔稳,調(diào)用對(duì)應(yīng)的Render
進(jìn)行繪制葛峻。
應(yīng)用
簡(jiǎn)單的靜態(tài)圖片的展示
- 創(chuàng)建
MovieMaker
就是使用之前創(chuàng)建好的Render
在對(duì)應(yīng)的生命周期方法調(diào)用。因?yàn)槭庆o態(tài)圖片巴比。所以這里沒(méi)有進(jìn)行變化术奖。
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 = "當(dāng)前進(jìn)度是" + (current * 1f / totalDuration * 1f);
textView.setText(text);
}
}).build();
engine.make();
}
- 結(jié)果
每三秒切換靜態(tài)圖片。
添加類似抖音的動(dòng)態(tài)變化
因?yàn)閯?dòng)畫(huà)效果轻绞,需要同時(shí)對(duì)兩圖進(jìn)行效果采记。所以需要兩個(gè)不同的Render
進(jìn)行變化。
- 定義動(dòng)態(tài)的
MovieMaker
- 構(gòu)造方法
public AnimateGroupPhotoMaker(String... filePaths) {
this.filePaths = new ArrayList<>();
this.filePaths.addAll(Arrays.asList(filePaths));
}
- 做矩陣變化完成政勃,動(dòng)畫(huà)
因?yàn)槲覀円呀?jīng)預(yù)留好了傳入時(shí)間的變化唧龄,所以只要根據(jù)這個(gè)時(shí)間變化,進(jìn)行變化矩陣就可以了奸远。
@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();
}
}
//進(jìn)行動(dòng)畫(huà)的變化
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) {
//第一個(gè)做縮小的動(dòng)畫(huà)
case 0:
v = 1f - dif * 0.1f;
Matrix.scaleM(mModelMatrix, 0, v, v, 0f);
photoFilter.setAlpha(1 - dif * 0.5f);
break;
//第二個(gè)做平移的動(dòng)畫(huà)
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é)合原來(lái)靜態(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動(dòng)畫(huà)切換讽挟。
源碼
文中Demo源碼的github地址
系列文章地址
Android OpenGL ES(一)-開(kāi)始描繪一個(gè)平面三角形
Android OpenGL ES(二)-正交投影
Android OpenGL ES(三)-平面圖形
Android OpenGL ES(四)-為平面圖添加濾鏡
Android OpenGL ES(五)-結(jié)合相機(jī)進(jìn)行預(yù)覽/錄制及添加濾鏡
Android OpenGL ES(六) - 將輸入源換成視頻
Android OpenGL ES(七) - 生成抖音照片電影