Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Android OpenGL ES(七) - 生成抖音照片电影

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

作者头像
deep_sadness
发布于 2018-10-10 03:47:46
发布于 2018-10-10 03:47:46
2.1K00
代码可运行
举报
文章被收录于专栏:Flutter入门Flutter入门
运行总次数:0
代码可运行

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

  1. 创建 同时,通过mSurfaceHolder来创建EGLSurface

创建ElgSurface.png

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

录制部分

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

  • 那我们这儿又多了一个想要绘制的Surface要怎么办呢? 我们知道,绘制实际上是将缓存在纹理上的进行,进行输出。而纹理是和线程中的EglContext绑定。 所以,我们只要能得到这个结果的纹理,保持相同的EglContext,重新绘制一次,就有相同的结果了。 这样我们就可以利用EncoderInputSurface和相同的EglContext,来再次创建一个EglSurface。在这里绘制相同的纹理,就可以得到相同的结果。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//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就可以完成这样。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 //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.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private MovieHandler getMovieHandler() {
        if (mMovieHandler == null) {
            mMovieHandler = new MovieHandler(getLooper(), this);
        }
        return mMovieHandler;
    }
模仿Render,将绘制的流程解耦出来

这样就可以自由的进行绘制。 同时我们需要Duration的属性,这样我们能在正确的时间范围内,取到我们想要的Render和让Render针对时间进行变形。 绘制的方法,同时加上当前的时间戳

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public interface MovieMaker {

    long ONE_BILLION = 1000000000;

    void onGLCreate();

    void setSize(int width, int height);

    long getDurationAsNano();

    void generateFrame(long curTime);

    void release();
}
整体的绘制流程
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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在对应的生命周期方法调用。因为是静态图片。所以这里没有进行变化。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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();
    }
}
  • 调用
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  @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
  • 构造方法
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public AnimateGroupPhotoMaker(String... filePaths) {
        this.filePaths = new ArrayList<>();
        this.filePaths.addAll(Arrays.asList(filePaths));
    }
  • 做矩阵变化完成,动画 因为我们已经预留好了传入时间的变化,所以只要根据这个时间变化,进行变化矩阵就可以了。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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. 使用
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
   @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地址

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018.09.26 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
三万字带你了解那些年面过的Java八股文[通俗易懂]
国内的互联网面试,恐怕是现存的、最接近科举考试的制度。而且,我国的八股文(基础知识、集合框架、多线程、线程的五种状态、虚拟机、MySQL、Spring相关、计算机网络、MQ系列等)确实是独树一帜。以美国为例,北美工程师面试比较重视算法(Coding),近几年也会加入Design轮(系统设计和面向对象设计OOD)和BQ轮(Behavioral question,行为面试问题),今天博主为大家熬断半头青丝捋一捋这现代八股文
全栈程序员站长
2022/09/05
2.3K0
Java基础--2021Java面试题系列教程--大白话解读
本教程是系列教程,包含 Java 基础,JVM,容器,多线程,反射,异常,网络,对象拷贝,JavaWeb,设计模式,Spring-Spring MVC,Spring Boot / Spring Cloud,Mybatis / Hibernate,Kafka,RocketMQ,Zookeeper,MySQL,Redis,Elasticsearch,Lucene
JavaPub
2021/01/27
5081
Java基础八股文第一弹
春招来啦,今天给大家分享Java基础高频面试题(第一弹),希望小伙伴们看完之后面试稳过!
程序员大彬
2022/07/08
1K0
Java基础八股文(背诵版)
二哥整理了一份 Java 基础篇的八股文,大家在面试前可以背一遍,一定能“吊打”面试官。
沉默王二
2021/09/03
45.3K1
Java基础八股文(背诵版)
JAVA面试备战(一)--java 基础
基本类型保存原始值,引用类型保存的是引用值(引用值就是指对象在堆中所 处的位置/地址)
程序员爱酸奶
2022/04/12
5130
JAVA面试备战(一)--java 基础
Java面试集合(七)
Java面试集合(六) 的回顾,对于final可以修饰常量,方法,和类,一旦常量定义好后就不可改变,而方法,用final来修饰方法,方法不可重载,继承,重写,final用来修饰类,该类不能被继承。
达达前端
2019/07/03
5440
Java面试集合(七)
Java经典面试题答案解析(1-80题)
前几天,在茫茫的互联网海洋中寻寻觅觅,把收藏的800道Java经典面试题都发出来,有小伙伴私聊我要答案。所以感觉没有答案的面试题是没有灵魂的,于是今天先整理基础篇的前80道答案出来哈~
捡田螺的小男孩
2020/05/22
6750
Java面试——Java基础
Java语言中一共提供了8种原始的数据类型(byte,short,int,long,float,double,char,boolean),这些数据类型不是对象,而是 Java语言中不同于类的特殊类型,这些基本类型的数据变量在声明之后就会立刻在栈上被分配内存空间。除了这8种基本的数据类型外,其他类型都是引用类型(例如类、接口、数组等),引用类型类似于C++中的引用或指针的概念,它以特殊的方式指向对象实体,此类变量在声明时不会被分配内存空间,只是存储了一个内存地址而已。
Java架构师必看
2021/05/14
3K0
Java面试——Java基础
《面试八股文》之 Java 基础 34 卷
「《面试八股文》之 Java 基础 34卷」 它来了,本文总共针对基础给了 34 个问题,又是小小 1W 字,理解它,看透它~
moon聊技术
2021/10/22
1.4K0
Java 基础面试总结
类的成员不写访问修饰时默认为default。默认对于同一个包中的其他类相当于公开(public),对于不是同一个包中的其他类相当于私有(private)。受保护(protected)对子类相当于公开,对不是同一包中的没有父子关系的类相当于私有。
Tim在路上
2020/08/04
6090
【BATJ面试必会】Java 基础篇(一)
基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。
乔戈里
2019/02/27
4920
Java 基础 - 知识点
基本数据类型包括 boolean(布尔型)、float(单精度浮点型)、char(字符型)、byte(字节型)、short(短整型)、int(整型)、long(长整型)和 double (双精度浮点型)共 8 种。
郭顺发
2021/12/17
6460
Java 基础 - 知识点
460道Java后端面试高频题答案版【模块一:Java基础】
面向对象是一种基于面向过程的编程思想,是向现实世界模型的自然延伸,这是一种“万物皆对象”的编程思想。由执行者变为指挥者,在现实生活中的任何物体都可以归为一类事物,而每一个个体都是一类事物的实例。面向对象的编程是以对象为中心,以消息为驱动。
乔戈里
2019/09/24
1K0
460道Java后端面试高频题答案版【模块一:Java基础】
经典Java面试题收集
2、访问修饰符public,private,protected,以及不写(默认)时的区别?
nnngu
2018/03/14
1.6K0
经典Java面试题收集
25道Java基础面试题
如果值的范围在-128到127之间,它就从高速缓存返回实例。否则 new 一个Integer对象。new Integer 就是一个装箱的过程了,装箱的过程会创建对应的对象,这个会消耗内存,所以装箱的过程会增加内存的消耗,影响性能。
HaC
2020/12/30
4410
25道Java基础面试题
「万字图文」史上最姨母级Java继承详解
在Java课堂中,所有老师不得不提到面向对象(Object Oriented),而在谈到面向对象的时候,又不得不提到面向对象的三大特征:封装、继承、多态。三大特征紧密联系而又有区别,本课程就带你学习Java的继承。
bigsai
2020/11/19
4420
「万字图文」史上最姨母级Java继承详解
【2022最新Java面试宝典】—— Java基础知识面试题(91道含答案)
所谓跨平台性,是指java语言编写的程序,一次编译后,可以在多个系统平台上运行。 实现原理:Java程序是通过java虚拟机在系统平台上运行的,只要该系统可以安装相应的java虚拟 机,该系统就可以运行java程序。
全栈程序员站长
2022/11/06
7350
【2022最新Java面试宝典】—— Java基础知识面试题(91道含答案)
程序员的56大Java基础面试问题及答案
首先,属性可用来描述同一类事物的特征,方法可描述一类事物可做的操作。封装就是把属于同一类事物的共性(包括属性与方法)归到一个类中,以方便使用。
鱼找水需要时间
2023/12/23
2090
程序员的56大Java基础面试问题及答案
Java面试题及答案2019版(上)
抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段(如果不能理解请阅读阎宏博士的《Java与模式》或《设计模式精解》中关于桥梁模式的部分)。封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口(可以想想普通洗衣机和全自动洗衣机的差别,明显全自动洗衣机封装更好因此操作起来更简单;我们现在使用的智能手机也是封装得足够好的,因为几个按键就搞定了所有的事情)。多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的服务,那么运行时的多态性可以解释为:当A系统访问B系统提供的服务时,B系统有多种提供服务的方式,但一切对A系统来说都是透明的(就像电动剃须刀是A系统,它的供电系统是B系统,B系统可以使用电池供电或者用交流电,甚至还有可能是太阳能,A系统只会通过B类对象调用供电的方法,但并不知道供电系统的底层实现是什么,究竟通过何种方式获得了动力)。方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:1). 方法重写(子类继承父类并重写父类中已有的或抽象的方法);2). 对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。
用户7886150
2020/12/10
5390
Java 基础常见知识点&面试题总结(中),2022 最新版!
你好,我是 Guide。秋招即将到来,我对 JavaGuide 的内容进行了重构完善,公众号同步一下最新更新,希望能够帮助你。
Guide哥
2022/11/07
4300
Java 基础常见知识点&面试题总结(中),2022 最新版!
推荐阅读
相关推荐
三万字带你了解那些年面过的Java八股文[通俗易懂]
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验