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

image.png

之前我們結(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)題:

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

直接生成影片

OpenGL繪制

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

通過(guò)之前的學(xué)習(xí)绒疗,我們通過(guò)閱讀源碼和文章侵歇,能夠了解到整個(gè)OpenGL繪制的流程時(shí)這樣的。

image.png

之前文章中寫到的這些部分吓蘑,都是直接由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)中琳猫。

GLSurfaceViewEGLSurface是怎么關(guān)聯(lián)的呢伟叛?
  1. 繼承
    通過(guò)閱讀源碼可以看到,GLSurfaceView直接繼承了SurfaceView
    繼承SurfaceView.png
  2. 創(chuàng)建
    同時(shí),通過(guò)mSurfaceHolder來(lái)創(chuàng)建EGLSurface
    創(chuàng)建ElgSurface.png

這樣脐嫂,使用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í)輸入到了EncoderSurface當(dāng)中了鞭衩。

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

因?yàn)閯?dòng)畫(huà)效果轻绞,需要同時(shí)對(duì)兩圖進(jìn)行效果采记。所以需要兩個(gè)不同的Render進(jìn)行變化。

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

  1. 結(jié)果
    每三秒靜態(tài)圖片和0.35s動(dòng)畫(huà)切換讽挟。


    movie-ge-2.gif

源碼

文中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(七) - 生成抖音照片電影

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市丸冕,隨后出現(xiàn)的幾起案子耽梅,更是在濱河造成了極大的恐慌,老刑警劉巖胖烛,帶你破解...
    沈念sama閱讀 218,036評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件眼姐,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡佩番,警方通過(guò)查閱死者的電腦和手機(jī)妥凳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)答捕,“玉大人逝钥,你說(shuō)我怎么就攤上這事」案洌” “怎么了艘款?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,411評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)沃琅。 經(jīng)常有香客問(wèn)我哗咆,道長(zhǎng),這世上最難降的妖魔是什么益眉? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,622評(píng)論 1 293
  • 正文 為了忘掉前任晌柬,我火速辦了婚禮,結(jié)果婚禮上郭脂,老公的妹妹穿的比我還像新娘年碘。我一直安慰自己,他們只是感情好展鸡,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布屿衅。 她就那樣靜靜地躺著,像睡著了一般莹弊。 火紅的嫁衣襯著肌膚如雪涤久。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,521評(píng)論 1 304
  • 那天忍弛,我揣著相機(jī)與錄音响迂,去河邊找鬼。 笑死细疚,一個(gè)胖子當(dāng)著我的面吹牛蔗彤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,288評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼幕与,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了镇防?” 一聲冷哼從身側(cè)響起啦鸣,我...
    開(kāi)封第一講書(shū)人閱讀 39,200評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎来氧,沒(méi)想到半個(gè)月后诫给,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,644評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡啦扬,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評(píng)論 3 336
  • 正文 我和宋清朗相戀三年中狂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扑毡。...
    茶點(diǎn)故事閱讀 39,953評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡胃榕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瞄摊,到底是詐尸還是另有隱情勋又,我是刑警寧澤,帶...
    沈念sama閱讀 35,673評(píng)論 5 346
  • 正文 年R本政府宣布换帜,位于F島的核電站楔壤,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏惯驼。R本人自食惡果不足惜蹲嚣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望祟牲。 院中可真熱鬧隙畜,春花似錦、人聲如沸说贝。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,889評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)狂丝。三九已至换淆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間几颜,已是汗流浹背倍试。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,011評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蛋哭,地道東北人县习。 一個(gè)月前我還...
    沈念sama閱讀 48,119評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親躁愿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子叛本,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評(píng)論 2 355