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

image.png

之前我们结合相机和视频,结合滤镜,做了实时的预览和录制。
这期,我们来试试利用OpenGL+MediaCodc,不进行预览直接录制成视频的情况。

两个问题

录制视频的开始,我们先来思考两个问题:

  1. 如何直接生成影片。(不同于之前边预览边录制的流程)
  2. 如何确定影片的帧数。(不同于之前,都是通过Api通知,完成帧之后的回调)

直接生成影片

OpenGL绘制

参考 从源码角度剖析Android系统EGL及GL线程

通过之前的学习,我们通过阅读源码和文章,能够了解到整个OpenGL绘制的流程时这样的。

image.png

之前文章中写到的这些部分,都是直接由GLSurfaceView帮我们完成了。

预览部分 - 手机屏幕上显示

之前的预览部分都是直接使用GLSurfaceView
因为GLSurfaceView已经为我们当前的线程准备好了EGL的环境。所以我们只要生成自己的纹理texture,并进行绘制就可以了。
绘制的结果,就会出现在准备好的EGLSurface当中。

GLSurfaceViewEGLSurface是怎么关联的呢?
  1. 继承
    通过阅读源码可以看到,GLSurfaceView直接继承了SurfaceView
    继承SurfaceView.png
  2. 创建
    同时,通过mSurfaceHolder来创建EGLSurface
    创建ElgSurface.png

这样,使用draw之后,通过eglSwapBuffers,就会将内容绘制到GLSurfaceView当中。

录制部分

通过预览部分的回顾,我们知道,通过用SurfaceView进行创建和关联EGLSurface,就可以绘制到整个SurfaceView上。er实际上,录制就是同时输入到了EncoderSurface当中了。

  • 那我们这儿又多了一个想要绘制的Surface要怎么办呢?
    我们知道,绘制实际上是将缓存在纹理上的进行,进行输出。而纹理是和线程中的EglContext绑定。
    所以,我们只要能得到这个结果的纹理,保持相同的EglContext,重新绘制一次,就有相同的结果了。
    这样我们就可以利用EncoderInputSurface和相同的EglContext,来再次创建一个EglSurface。在这里绘制相同的纹理,就可以得到相同的结果。
//1 . 创建
//得到当前线程的EGLContext
EGL14.eglGetCurrentContext();
//在新的线程中,进行创建新的 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();

对比

对比,我们就能发现。

  • 要在屏幕上显示,需要使用SurfaceView或其他Android原生的View来创建对应的EGLSurface
  • 利用Encoder进行录制,我们只需要利用它的InputSurface来创建,EGLSurface就可以了。

这里有个问题。如果我们想要使用FFmpeg,并且不使用Camera的回调来接受数据的话,要怎么办呢?

确定影片的帧数(绘制的时机)

通常的影片的帧数(fps)都是30。所以我们只要保持编码时,输入的时间戳是相隔30fps就可以完成这样。

 //fps 30
    private long computePresentationTimeNsec(int frameIndex) {
        final long ONE_BILLION = 1000000000;
        return frameIndex * ONE_BILLION / 30;
    }

整体

整个流程需要异步。和UI回调

直接使用了HandlerThread。和使用MainLooper来创建Handler就可以完成。
这里需要注意的是,进行线程通信时,要确保内部的Handler已经创建,需要进行getLooper()之后,来创建Handler.
这里的getLooper()是一个同步的方法,只要当前的Thread不是结束的状态,就能确保得到非空的Looper.

private MovieHandler getMovieHandler() {
        if (mMovieHandler == null) {
            mMovieHandler = new MovieHandler(getLooper(), this);
        }
        return mMovieHandler;
    }

模仿Render,将绘制的流程解耦出来

这样就可以自由的进行绘制。
同时我们需要Duration的属性,这样我们能在正确的时间范围内,取到我们想要的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环境
            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 {
            //结束
            try {
                releaseEncoder();
            } catch (Exception e) {
                e.printStackTrace();
            }
            if (isCompleted && listener != null) {
                uiHandler.post(() -> {
                    listener.onCompleted(outputFile.getAbsolutePath());
                });
            }
        }

    }

同样是先创建对应的EGL环境。然后在给定的时长下,调用对应的Render进行绘制。

应用

简单的静态图片的展示
  • 创建MovieMaker
    就是使用之前创建好的Render在对应的生命周期方法调用。因为是静态图片。所以这里没有进行变化。
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();
    }
}
  • 调用
  @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();
    }
  • 结果
    每三秒切换静态图片。
movie-ge-1.gif
添加类似抖音的动态变化

因为动画效果,需要同时对两图进行效果。所以需要两个不同的Render进行变化。

  1. 定义动态的MovieMaker
  • 构造方法
    public AnimateGroupPhotoMaker(String... filePaths) {
        this.filePaths = new ArrayList<>();
        this.filePaths.addAll(Arrays.asList(filePaths));
    }
  • 做矩阵变化完成,动画
    因为我们已经预留好了传入时间的变化,所以只要根据这个时间变化,进行变化矩阵就可以了。
@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()
                //结合原来静态的图片显示。组成幻灯片的效果
                .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. 结果
    每三秒静态图片和0.35s动画切换。


    movie-ge-2.gif

源码

文中Demo源码的github地址

系列文章地址
Android OpenGL ES(一)-开始描绘一个平面三角形
Android OpenGL ES(二)-正交投影
Android OpenGL ES(三)-平面图形
Android OpenGL ES(四)-为平面图添加滤镜
Android OpenGL ES(五)-结合相机进行预览/录制及添加滤镜
Android OpenGL ES(六) - 将输入源换成视频
Android OpenGL ES(七) - 生成抖音照片电影

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266