Android OpenGL ES(七) - 生成抖音照片電影

image.png

之前我們結(jié)合相機和視頻,結(jié)合濾鏡,做了實時的預(yù)覽和錄制。
這期,我們來試試利用OpenGL+MediaCodc,不進行預(yù)覽直接錄制成視頻的情況。

兩個問題

錄制視頻的開始,我們先來思考兩個問題:

  1. 如何直接生成影片。(不同于之前邊預(yù)覽邊錄制的流程)
  2. 如何確定影片的幀數(shù)。(不同于之前,都是通過Api通知,完成幀之后的回調(diào))

直接生成影片

OpenGL繪制

參考 從源碼角度剖析Android系統(tǒng)EGL及GL線程

通過之前的學習,我們通過閱讀源碼和文章,能夠了解到整個OpenGL繪制的流程時這樣的。

image.png

之前文章中寫到的這些部分,都是直接由GLSurfaceView幫我們完成了。

預(yù)覽部分 - 手機屏幕上顯示

之前的預(yù)覽部分都是直接使用GLSurfaceView。
因為GLSurfaceView已經(jīng)為我們當前的線程準備好了EGL的環(huán)境。所以我們只要生成自己的紋理texture,并進行繪制就可以了。
繪制的結(jié)果,就會出現(xiàn)在準備好的EGLSurface當中。

GLSurfaceViewEGLSurface是怎么關(guān)聯(lián)的呢?
  1. 繼承
    通過閱讀源碼可以看到,GLSurfaceView直接繼承了SurfaceView
    繼承SurfaceView.png
  2. 創(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實際上,錄制就是同時輸入到了EncoderSurface當中了。

  • 那我們這兒又多了一個想要繪制的Surface要怎么辦呢?
    我們知道,繪制實際上是將緩存在紋理上的進行,進行輸出。而紋理是和線程中的EglContext綁定。
    所以,我們只要能得到這個結(jié)果的紋理,保持相同的EglContext,重新繪制一次,就有相同的結(jié)果了。
    這樣我們就可以利用EncoderInputSurface和相同的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)圖片。
movie-ge-1.gif
添加類似抖音的動態(tài)變化

因為動畫效果,需要同時對兩圖進行效果。所以需要兩個不同的Render進行變化。

  1. 定義動態(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);
    }
  1. 使用
   @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();
    }

  1. 結(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(七) - 生成抖音照片電影

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容