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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【Android 音视频开发打怪升级:OpenGL渲染视频画面篇】四、深入了解OpenGL之EGL
前面的文章就介绍过,OpenGL是基于线程的,直到目前为止,我们并没有深刻的认识到这个问题,但我们知道的是,当我们继承GLSurfaceView.Renderer时,系统会回调以下方法:
开发的猫
2020/04/02
2.7K0
【Android 音视频开发打怪升级:OpenGL渲染视频画面篇】四、深入了解OpenGL之EGL
OpenGL ES 共享上下文实现多线程渲染
EGL 是 OpenGL ES 和本地窗口系统(Native Window System)之间的通信接口,它的主要作用:
字节流动
2021/06/04
6.8K0
学习 OpenGL ES 之前,你需要了解下 EGL
EGL 是 OpenGL ES 和本地窗口系统(Native Window System)之间的通信接口,它的主要作用:
字节流动
2021/11/10
4.1K1
Android自定义GLSurfaceView
当我们需要把同一个场景渲染到不同的Surface上时,此时系统GLSurfaceView 就不能满足需求了,所以我们需要自己创建EGL环境来实现渲染操作。 注意: OpenGL整体是一个状态机,通过改变状态就能改变后续的渲染方式,而 EGLContext(EgL上下文)就保存有所有状态,因此可以通过共享EGLContext 来实现同一场景渲染到不同的Surface上。
曾大稳
2018/09/11
1.7K0
Android自定义GLSurfaceView
OpenGL ES简介
概述 在聊Android的View渲染流程中,通常会有一个比较核心的步骤:通过OpeGL ES接口调用GPU接口通知GPU绘制图形。其完整的流程:UI对象—->CPU处理为多维图形,纹理 —–通过Op
xiangzhihong
2018/01/26
2.2K0
[-综合篇-] 相机、OpenGL、视频、Flutter和SurfaceView
认识一个类,相当于结交一位朋友;看一篇源码,相当于一次顶级的会话; 读一个框架,相当于见证一段思想;做一个程序,相当于创造一个生命; 一次Git提交,相当于记录一次成长;生活也许并非那么美好,但一切可以这么崇高。----张风捷特烈
张风捷特烈
2020/04/30
3.3K0
[-综合篇-] 相机、OpenGL、视频、Flutter和SurfaceView
Android OpenGL ES(六)-给相机添加滤镜
上文中我们已经实现了将OpenGL和相机结合到一起,本文就在上文的基础上,添加滤镜。
deep_sadness
2018/10/10
2.9K0
Android OpenGL ES(六)-给相机添加滤镜
【Android 音视频开发打怪升级:FFmpeg音视频编解码篇】
在 Java 层,Android 已经为我们提供了 GLSurfaceView 用于 OpenGL ES 的渲染,我们不必关心 OpenGL ES 中关于 EGL 部分的内容,也无需关注 OpenGL ES 的渲染流程。
开发的猫
2020/06/03
2.2K0
OpenGL ES——一个平平无奇的三角形
前言 随着VR/AR技术的普及,人机交互的模式将产生新的变革。OpenGL ES作为移动端上的图像渲染框架,将变得越来越重要。在此将学习OpenGL ES作为Q3的主要目标。在10月1日前,希望能有阶段性成果。 快速开始 判断设备是否支持OpenGL ES fun checkSupported() : Boolean{ var supportsEs2 = false; val activityManager = getSystemService(ACTIVITY_SE
Oceanlong
2018/07/03
8630
OpenGL ES 与 GLSurfaceView 渲染视频帧
大家好,本文是 iOS/Android 音视频专题 的第六篇,该专题中 AVPlayer 项目代码将在 Github 进行托管,你可在微信公众号(GeekDev)后台回复 资料 获取项目地址。
100001509164
2022/01/20
2.6K0
RenderDemo(1):用 OpenGL 画一个三角形丨音视频工程示例
这个公众号会路线图式的遍历分享音视频技术:音视频基础 → 音视频工具 → 音视频工程示例 → 音视频工业实战。关注一下成本不高,错过干货损失不小 ↓↓↓
关键帧
2022/11/29
1.3K0
RenderDemo(1):用 OpenGL 画一个三角形丨音视频工程示例
Android硬件加速(二)-RenderThread与OpenGL GPU渲染
Android4.0之后,系统默认开启硬件加速来渲染视图,之前,理解Android硬件加速的小白文简单的讲述了硬件加速的简单模型,不过主要针对前半阶段,并没怎么说是如何使用OpenGL、GPU处理数据的,OpenGL主要处理的任务有Surface的composition及图形图像的渲染,本篇文章简单说一下后半部分的模型,这部分对于理解View渲染也有不少帮助,也能更好的帮助理解GPU渲染玄学曲线。
看书的小蜗牛
2018/08/16
10.6K0
Android硬件加速(二)-RenderThread与OpenGL GPU渲染
Android 图形架构
要理解Android的图形架构,我们需要先理解window的概念。维基百科中给window的定义是:Window是图形用户界面(GUI)系统中显示器上一个单独的视图区域(可以想象你电脑桌面上一个个窗口)。
字节流动
2022/09/26
2.5K0
Android 基于OpenGl ES渲染yuv视频(十二)
本文是基于前面两篇OpenGl理论学习的实际应用,更好的巩固一下前面的学习内容,重点讲下如何使用OpenGl去渲染一个yuv格式视频。
PengJie
2021/01/14
2.5K0
TRTCSDK自定义采集Texture2D视频通话
摄像头通话功能,是TRTCSDK对系统摄像头进行了封装,采集摄像头数据,编码传输通话。
ppchao
2020/12/03
1.4K1
TRTCSDK自定义采集Texture2D视频通话
播放视频时如何在视频帧上添加水印
之前的一篇文章中我们介绍了播放视频的时候调整音频的音量,我们能否在播放视频的时候在视频画面上加上水印?
马上就说
2020/11/11
3.3K0
Android OpenGL ES开发初探
网上介绍很多,这里不多讲,直接简单的讲,OpenGL是一个可以用来画二维或者三维图形库。而OpenGL ES呢,是OpenGL针对嵌入式设备搞的一个库,所以移动开发上用的基本上就是OpenGL ES了。
Clayman Twinkle
2018/07/25
1.2K0
Android OpenGL ES开发初探
Android OpenGL ES入门
  OpenGL(Open Graphics Library)是一种用于渲染2D和3D图形的跨平台编程接口。OpenGL提供了一套标准的函数和接口,使开发人员能够在各种操作系统上创建高性能的图形应用程序,这些操作系统包括Windows、Linux、macOS和一些嵌入式系统。OpenGL ES(OpenGL for Embedded Systems)是OpenGL的嵌入式系统版本,专门设计用于移动设备、嵌入式系统和其他资源受限的环境。与标准的OpenGL相比,OpenGL ES经过精简和优化,以适应移动设备和嵌入式系统的硬件和性能要求。
故乡的樱花开了
2023/12/06
4250
OpenGL ES for Android 相机预览
Android上打开摄像头需要camera权限,在Android 6.0及以上的版本需要动态申请权限,在`AndroidManifest.xml`中添加camera权限:
老孟Flutter
2020/09/11
1.1K0
SurfaceView简单理解,Android混淆,Android openGl开发详解简单图形的绘制,
在要求实时性比较高的游戏开发中,显然,view的ondraw是满足不了你的,这时候只能是用SurfaceView。
zhangjiqun
2024/12/16
1710
SurfaceView简单理解,Android混淆,Android openGl开发详解简单图形的绘制,
推荐阅读
相关推荐
【Android 音视频开发打怪升级:OpenGL渲染视频画面篇】四、深入了解OpenGL之EGL
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验