Android之OpenGL学习(2)

本文介绍了OpenGL中的gl_Position在齐次坐标系中的作用,图元的概念,以及GPU的计算模式和着色器工作原理。作者还探讨了OpenGL版本支持问题,特别提到其GooglePixel设备仅支持OpenGL2.0。在视频播放部分,文章详细阐述了MediaCodec在Java和NDK中的使用,包括解码流程、数据处理,并提到了使用NDK进行音视频渲染的计划。
摘要由CSDN通过智能技术生成

1. 前言

在通宵看了书籍之后,接触了我不少的疑惑,于是我开始随手写下第二篇OpenGL学习记录。不仅仅是完成上篇的KPI,更是希望尽善尽美。话不多说,开始。

2. 正文

2.1 解答疑惑

2.1.1 gl_Position为什么使用vec4

我们在第一篇的vertex shader中赋值gl_Position是一个vec4,对吧?表示这个一个4个float的数组,但是我们的坐标系却只有xyz三个,那么第4个是什么呢?用来干什么的呢?我在书中得到了答案,这个gl_Position是属于齐次坐标系,而并非三维坐标系。两者之间的转换为三维坐标系(x/w,y/w,z/w)=齐次坐标系(x,y,z,w),当w为了1,那么齐次坐标系中的前三个数字是不变的,之所以选用为齐次坐标系,主要还是从两点出发:1.方便使用4*4的矩阵计算,因为在平移和旋转中矩阵运算更为快捷;2.为了在投影中进行仿射计算(参考资料:为什么directX里表示三维坐标要建一个4*4的矩阵?)

2.1.2 图元

第一篇中最后绘制铺满整个屏幕的四边形时,是分别绘制了两个三角形进行拼接的。我可以很明确说,OpenGL确实是只能绘制点、线和三角形,称之为图元,然后再由这些进行拼接而成。

参考图片来源(OpenGL-13-几何着色器

2.1.3 GPU的计算模式和着色器

首先就是GPU的计算模式,称之为管线(pipeline,流水线)。相当于Java中的stream,只不过每个stream都只有一个数据(一个坐标或者一个片段),把整个数据分成无数个stream进行并行计算,每个stream都会经历vertex shader\fragment shader等等这些。听起来就颇像stream并行。其次就是着色器了,我们一般只会使用到上述两种着色器,但是在OpenGL ESversion3.2中我们可以看到有GL_GEOMETRY_SHADER的定义,这几乎可以说,已经可以使用几何着色器,而几何着色器输入是一个图元的顶点坐标数据,输出则是也是一组图元的顶点坐标数据,但是我们却可以对其顶点坐标数据进行变换。

2.1.4 OpenGL版本

本人针对手上的设备进行检测后发现,我的Google Pixel居然只连OpenGL version2.0都不支持,版本是196610,所以上一篇文章中的shader source code才会报错。

marlin:/ # getprop  ro.opengles.version
196610

下面是算是检测代码。

private static class ContextFactory implements GLSurfaceView.EGLContextFactory {

        @Override
        public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
            int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, 3,
                    EGL10.EGL_NONE};
            EGLContext context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
            if (context == null) {
                // returns null if 3.0 is not supported;
                
            }
            return context;
        }

        @Override
        public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) {
            egl.eglDestroyContext(display, context);
        }
    }

当context为null是,就说明不支持。

2.2 视频播放

为了提高性能和学习,决定使用MediaCodec进行解码,然后使用OpenGL和Oboe分别进行音视频的渲染,视频采用H264和AAC编码的音视频文件,为此我们将转到NDK层开发。

2.2.1 MediaCodec的Java学习

我们需要回想一下MediaCodec在Java中的使用。首先就是使用MediaCodec自带的静态创建函数进行创建,一般参数为type(支持的type可以自己去看MediaFormat类的static final属性定义)。随后就是配置format,根据音频的不同各有不同,具体可以Google或者查看media codec官方demo(MediaCodec 低延时解码),最后使用MediaCodec的configure函数,到这里才算配置结束。

我们使用start函数开始工作,在这里我们需要获得两个ByteBuffer数组,一个是input,一个output。我们需要使用dequeue相关函数(函数全称并未这个,只是为了讲解而缩减的)来获取可用的bufferIndex,只不过使用之前都需要clear一下,免得之前的缓存数据造成影响,最后别忘记使用Release函数来结束使用。在最新的MediaCodec中可以直接使用设置callback,当Input or Output BufferAvailable时会自动回调,不需要再dequeue,而NDK中则也是如此。

2.2.2 MediaCodec的NDK开发

对视频进行解析,我们首先需要获取视频文件的fileDescriptor,然后导入这些关于MediaCodec头文件(关于Android中如何NDK开发,自行搜索资料配置即可)。

#include <media/NdkMediaExtractor.h>
#include <media/NdkMediaCodec.h>
#include <media/NdkMediaCrypto.h>
#include <media/NdkMediaFormat.h>
#include <media/NdkMediaMuxer.h>

我们首先使用AMediaExetractor_setDataSourceFd传入fileDescriptor来获取AMediaExetrator,然后再使用AMediaExtractor_getTrackCount来获取TrackCount,我这里测试的视频文件有3个track,因为一个封面,另外两个就是音视频了。

 使用AMediaExtractor_getTrackFormat来获取相对应Track的AMediaFormat,然后使用AMediaFormat_getString来获取mime类型,进行判断后就可以初始化AMediaCodec了,但是请一定不要忘记使用AMediaExtractor_selectTrack来选中Track,否则后面读取采样数据时会出现什么错误我也不知道。

请看类的定义

inkPlayer.h

class inkPlayer {
public:
    inkPlayer(int fd, int64_t offset, int64_t length, ANativeWindow *window);

    static void onInputAvailable(inkPlayer *ptr);

    static void onOutputAvailable(inkPlayer *ptr);

    void start();

    ~inkPlayer();

private:
    AMediaExtractor *extractor;
    AMediaCodec *videoMediaCodec;
    bool mSawInputEOS = false;
    bool mSawOutputEOS = false;
};

inkPlayer.cpp#construct function

    extractor = AMediaExtractor_new();
    media_status_t err = AMediaExtractor_setDataSourceFd(extractor, fd, offset, length);
    if (err != AMEDIA_OK) {
        LOGE("setDataSource error: %d", err);
        return;
    }
    size_t numTracks = AMediaExtractor_getTrackCount(extractor);
    const char *mime;
    for (uint i = 0; i < numTracks; i++) {
        AMediaFormat *format = AMediaExtractor_getTrackFormat(extractor, i);

        if (!AMediaFormat_getString(format, AMEDIAFORMAT_KEY_MIME, &mime)) {
            LOGE("no mime type");
            continue;
        }
        if (!strncmp(mime, "video/", 6)) {
            videoMediaCodec = AMediaCodec_createDecoderByType(mime);
            AMediaCodec_configure(videoMediaCodec, format, window, nullptr, 0);
            err = AMediaExtractor_selectTrack(extractor, i);
            if (err != AMEDIA_OK) {
                LOGE("AMediaExtractor_selectTrack error: %d", err);
                return;
            }
        } else if (!strncmp(mime, "audio/", 6)) {
            LOGE("not support audio");
        } else {
            LOGE("expected audio or video mime type, got %s", mime);
        }
        AMediaFormat_delete(format);
    }

其中还定义了onInputAvailable和onOutputAvailable两个静态成员函数,是为了方便直接setCallback,但是查询资料后得知,这样子无法做到高性能,于是便放弃了,但是却保留了这两个函数。我们看一下JNI出使唤接口。

extern "C"
JNIEXPORT void JNICALL
Java_com_yymjr_opengl_MainActivity_init(JNIEnv *env, jclass clazz, jobject fileDescriptor,
                                        jlong offset, jlong length, jobject surface) {
    if (fileDescriptor != nullptr) {
        ANativeWindow *window = ANativeWindow_fromSurface(env, surface);
        jclass fileDescriptorClazz = env->GetObjectClass(fileDescriptor);
        jfieldID descriptorId = env->GetFieldID(fileDescriptorClazz, "descriptor", "I");
        jint fd = env->GetIntField(fileDescriptor, descriptorId);
        player = new inkPlayer(fd, offset, length, window);
    }
}

我们使用Surface构造了一个ANativeWindow,其本质上是为了提高性能,因为如果我们采用传统的dequeueOutputBuffer相关流程,相当于我们会把数据从GPU里面拷贝出来,进行处理后又拷贝回去,本案例中无需对视频数据进行任何处理,所以才直接使用的surface,但是这样子却也失去了灵活性。因为我们传入数据后,surface绑定了texture,直接会更新到屏幕,这样子我们算是对屏幕的刷新失去了控制(PS:虽然我们可以在传入数据上进行控制,但是这也极其不优雅,所以有没有什么好办法呢?)。反射获取fd,这也没有什么太好说。

inkPlayer.cpp#OnInputAvailable function

void inkPlayer::onInputAvailable(inkPlayer *ptr) {
    while (!ptr->mSawInputEOS) {
        ssize_t index = AMediaCodec_dequeueInputBuffer(ptr->videoMediaCodec, 5000);
        if (index < 0) continue;
        size_t bufSize;
        uint8_t *buf = AMediaCodec_getInputBuffer(ptr->videoMediaCodec, index, &bufSize);
        int sampleSize = AMediaExtractor_readSampleData(ptr->extractor, buf, bufSize);
        if (sampleSize < 0) {
            sampleSize = 0;
            ptr->mSawInputEOS = true;
            LOGV("EOS");
        }
        int64_t presentationTimeUs = AMediaExtractor_getSampleTime(ptr->extractor);
        LOGV("read sampleSize:%d PTS:%lld", sampleSize, presentationTimeUs);
        media_status_t err = AMediaCodec_queueInputBuffer(ptr->videoMediaCodec, index, 0,
                                                          sampleSize,
                                                          presentationTimeUs,
                                                          ptr->mSawInputEOS
                                                          ? AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM
                                                          : 0);
        if (err != AMEDIA_OK) {
            LOGE("AMediaCodec_queueInputBuffer error: %d", err);
        }
        AMediaExtractor_advance(ptr->extractor);
    }
}

其中getSampleData和queue Buffer没有什么太好说,比较常规。但是你需要知道两个关键点,那就是帧和PTS,首先就是这个是按照帧来获取采样数据,而advance则就是。唯一需要知道的就是advance,这是处理下一个帧(请注意,并不一定连续)。首先由于视频的压缩,导致了视频的帧类型分为了I、P、B三种类型,我这里大概说一下。首先I帧是完整的帧,I帧可以直接解码;但是P帧就是向前预测帧,它需要在它前面的I或者P帧的帮助才能解码;最后B帧就是双向预测帧,P帧需要在它前面的IorP帧加上后面P帧才能解码成功。以我的测试数据为例,他的I帧是0帧,P帧是第4帧,而123帧是B帧。帧顺序为(IBBBP),但是我们获取sample数据时,这个顺序就是IPBBB。我打印的日志。

V/InkPlayer: read sampleSize:100664 PTS:0
V/InkPlayer: read sampleSize:28886 PTS:320000
V/InkPlayer: read sampleSize:4646 PTS:160000
V/InkPlayer: read sampleSize:2020 PTS:80000
V/InkPlayer: read sampleSize:1619 PTS:40000
V/InkPlayer: read sampleSize:537 PTS:120000
V/InkPlayer: read sampleSize:1583 PTS:240000
V/InkPlayer: read sampleSize:1666 PTS:200000
V/InkPlayer: read sampleSize:1234 PTS:280000

后面的PTS则是显示时间戳,单位时us。我们按照PTS来的话,那么就应该是0,40000,800000,120000,160000,200000...但是你会发现这个顺序完全是错乱的(PS:该视频帧率为25FPS,1000ms/25=40ms=4000us,这就是为什么间隔为4000us的原因)

所以请注意这个问题,但是本人就懒得处理了。等待后续看会不会去做时间同步吧。

inkPlayer.cpp#onOutputAvailable function

void inkPlayer::onOutputAvailable(inkPlayer *ptr) {
    AMediaCodecBufferInfo info;
    while (!ptr->mSawOutputEOS) {
        ssize_t index = AMediaCodec_dequeueOutputBuffer(ptr->videoMediaCodec, &info, 5000);
        if (index < 0) continue;
        if (info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) {
            LOGV("EOS on track");
            ptr->mSawOutputEOS = true;
        }
        LOGV("got decoded buffer for size %d", info.size);
        AMediaCodec_releaseOutputBuffer(ptr->videoMediaCodec, index, true);
    }
}

其中主要是使用releaseOutputBuffer释放buffer。

inkPlayer.cpp#start function

void inkPlayer::start() {
    AMediaCodec_start(videoMediaCodec);
    pthread_t tid;
    int ref = pthread_create(&tid, nullptr, OnInputAvailableCB, this);
    if (ref != 0) {
        LOGV("create thread failed.");
    }
    ref = pthread_create(&tid, nullptr, OnOutputAvailableCB, this);
    if (ref != 0) {
        LOGV("create thread failed.");
    }
}

创建线程来执行上述两个对Buffer操作,但是本人也在思考,能不能getSampleData单独一个线程,然后使用vector来存储,然后再进行音视频同步。把InputBuffer和OutputBuffer操作放进协程里面,只不过目前c++好像还没有协程库。最后请大家一定不要放弃release操作。

inkPlayer.cpp#deconstructor function

inkPlayer::~inkPlayer() {
    AMediaCodec_delete(videoMediaCodec);
    AMediaExtractor_delete(extractor);
}

接下来我们重新把目光转回Java层面,首先第一个需要修改的地方就是fragment shader source code。

    final static String fragmentShaderSource = "#extension GL_OES_EGL_image_external : require\n" +
            "varying vec2 TexCoord;" +
            "uniform samplerExternalOES inTexture;\n" +
            "void main() {" +
            "   gl_FragColor = texture2D(inTexture, TexCoord);" +
            "}";

其中把inTexture改成sampleExternalOES,别忘记把在bind texture中也一起修改咯。

                mSurfaceTexture = new SurfaceTexture(textureId);
                mSurface = new Surface(mSurfaceTexture);
                mSurfaceTexture.updateTexImage();
                setDataSource(fileDescriptor, mSurface);

而textureId和surface的操作就是如此的朴实无华。在setDataSource中则是进行了inkPlyer的初始化操作。最后老规矩,上一张图片看下成果。

3.结尾

不知不觉又是八千字了,如果不出意料的话,应该是还会有第三篇文章的,下一篇主要还是音频解码,音视频同步应该会在下一篇中解决。其中关于多纹理和滤镜,估计会放到第四篇文章了。随后就是github地址(GitHub - yymjr/OpenGl),我们下一篇文章再见!欢迎大家指正我的错误,可以直接留言或者直接邮箱(yymjr@outlook.com)。

敬颂春祺,肃请夏安!

2022年03月20日

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值