Android使用OpenGL渲染ffmpeg解碼的YUV視頻數據

在《安卓使用SurfaceView繪制ffmpeg解碼的視頻數據》雖然我們成功地實現了視頻的渲染,
但是在YUV轉換成RGB的時候,我們調用了ffmpeg內部的轉換函數,這里面包含大量的計算轉換,
所以是很耗費CPU性能的。

今天我們來學習一下如何將YUV轉換RGB的功能轉換到GPU中去執行,減少CPU的計算工作量,達到性能優化的目的。

解決方案是使用OpenGL渲染,將YUV轉換RGB的功能交由著色器去處理。

Android中引入OpenGL

下面以OpenGL 2.0為例。

1、 引入庫文件

CMakeLists.txt引入GLESv2EGL庫(在ndk中內置)。
直接target_link_libraries加入即可:

target_link_libraries(
                      ........
                       #引入opengl的相關庫
                       GLESv2
                       EGL
                          )

2、 引入相關的頭文件

需要引入的頭文件

#include <EGL/egl.h>
#include <GLES2/gl2.h>

至此,我們的安卓OpenGL環境就算引入成功了。

OpenGL渲染環境搭建

1、 EGLDisplay
OpenGL(移動端稱作的是EGL)要知道把目標內容繪制在哪里。這就是EGLDisplay所需的功能。
EGLDisplay是一個封裝系統物理屏幕的數據類型(可以理解為繪制目標的一個抽象),通常會調用eglGetDisplay方法返回EGLDisplay來作為OpenGL ES渲染的目標。然后通過eglInitialize初始化顯示設備。

代碼如下:

//1 EGL display創建和初始化
    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    if (display == EGL_NO_DISPLAY) {
        LOGE("eglGetDisplay failed!");
        return;
    }
    if (EGL_TRUE != eglInitialize(display, 0, 0)) {
        LOGE("eglInitialize failed!");
        return;
    }

2、 EGLConfig
EGL有了Display之后,它就可以將OpenGL ES的輸出和設備的屏幕橋接起來,但是需要指定一些配置項,這時候EGLConfig就閃亮登場啦。

//輸出配置
    EGLConfig config;
    EGLint configNum;
    EGLint configSpec[] = {
            EGL_RED_SIZE, 8,
            EGL_GREEN_SIZE, 8,
            EGL_BLUE_SIZE, 8,
            EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_NONE
    };
    if (EGL_TRUE != eglChooseConfig(display, configSpec, &config, 1, &configNum)) {
        LOGE("eglChooseConfig failed!");
        return;
    }

3、 創建EGLSurface
有了顯示設備,那么如何將設備的屏幕與EGL鏈接起來呢?EGLSurface粉墨登場。

//獲取原始窗口
//surface是外部的SurfaceView傳遞進來的
ANativeWindow *nwin = ANativeWindow_fromSurface(env, surface);
//創建surface
EGLSurface winsurface = eglCreateWindowSurface(display, config, nwin, 0);
    if (winsurface == EGL_NO_SURFACE) {
        LOGE("eglCreateWindowSurface failed!");
        return;
    }

4、 EGLContext
OpenGL所創建的資源, 其實對程序員可見的僅僅是ID而已, 要操作其中內容就需要依賴于EGLContext上下文。

 //context 創建關聯的上下文
    const EGLint ctxAttr[] = {
            EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE
    };
    EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctxAttr);
    if (context == EGL_NO_CONTEXT) {
        LOGE("eglCreateContext failed!");
        return;
    }
    if (EGL_TRUE != eglMakeCurrent(display, winsurface, winsurface, context)) {
        LOGE("eglMakeCurrent failed!");
        return;
    }

4、 著色器
所有的渲染都需要位置信息與色彩信息這兩個信息才能成功渲染處理。頂點著色器和片元著色器就完成了這兩個功能。

頂點著色器:

 attribute  vec4 aPosition; //頂點坐標,在外部獲取傳遞進來

attribute vec2 aTexCoord; //材質(紋理)頂點坐標

varying vec2 vTexCoord;   //輸出的材質(紋理)坐標,給片元著色器使用

void main() {
      //紋理坐標轉換,以左上角為原點的紋理坐標轉換成以左下角為原點的紋理坐標,
      // 比如以左上角為原點的(0,0)對應以左下角為原點的紋理坐標的(0,1)
      vTexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
      gl_Position = aPosition;
    }

片元著色器:

precision mediump float;    //精度

        varying vec2 vTexCoord;     //頂點著色器傳遞的坐標,相同名字opengl會自動關聯

        uniform sampler2D yTexture; //輸入的材質(不透明灰度,單像素)

        uniform sampler2D uTexture;

        uniform sampler2D vTexture;
        void main() {
            vec3 yuv;
            vec3 rgb;
            yuv.r = texture2D(yTexture, vTexCoord).r; // y分量
            // 因為UV的默認值是127,所以我們這里要減去0.5(OpenGLES的Shader中會把內存中0~255的整數數值換算為0.0~1.0的浮點數值)
            yuv.g = texture2D(uTexture, vTexCoord).r - 0.5; // u分量
            yuv.b = texture2D(vTexture, vTexCoord).r - 0.5; // v分量
            // yuv轉換成rgb,兩種方法,一種是RGB按照特定換算公式單獨轉換
            // 另外一種是使用矩陣轉換
            rgb = mat3(1.0, 1.0, 1.0,
                       0.0, -0.39465, 2.03211,
                       1.13983, -0.58060, 0.0) * yuv;
            //輸出像素顏色
            gl_FragColor = vec4(rgb, 1.0);
        }

著色器編寫好之后如何使用呢?我們看一張圖:


著色器編譯關聯

根據圖片流程,我們很方便就創建編譯并且鏈接好著色器。

GLint InitShader(const char *code, GLint type) {
    //創建shader
    GLint sh = glCreateShader(type);
    if (sh == 0) {
        LOGE("glCreateShader %d failed!", type);
        return 0;
    }
    //加載shader
    glShaderSource(sh,
                   1,    //shader數量
                   &code, //shader代碼
                   0);   //代碼長度
    //編譯shader
    glCompileShader(sh);

    //獲取編譯情況
    GLint status;
    glGetShaderiv(sh, GL_COMPILE_STATUS, &status);
    if (status == 0) {
        LOGE("glCompileShader failed!");
        return 0;
    }
    LOGE("glCompileShader success!");
    return sh;
}

........此處省略若干代碼

    //頂點和片元shader初始化
    //頂點shader初始化
    GLint vsh = InitShader(vertexShader, GL_VERTEX_SHADER);
    //片元yuv420 shader初始化
    GLint fsh = InitShader(fragYUV420P, GL_FRAGMENT_SHADER);


    /////////////////////////////////////////////////////////////
    //創建渲染程序
    GLint program = glCreateProgram();
    if (program == 0) {
        LOGE("glCreateProgram failed!");
        return;
    }
    //渲染程序中加入著色器代碼
    glAttachShader(program, vsh);
    glAttachShader(program, fsh);

    //鏈接程序
    glLinkProgram(program);
    GLint status = 0;
    glGetProgramiv(program, GL_LINK_STATUS, &status);
    if (status != GL_TRUE) {
        LOGE("glLinkProgram failed!");
        return;
    }
    glUseProgram(program);
    LOGE("glLinkProgram success!");
    /////////////////////////////////////////////////////////////


    //加入三維頂點數據 兩個三角形組成正方形
    static float vers[] = {
            1.0f, -1.0f, 0.0f,
            -1.0f, -1.0f, 0.0f,
            1.0f, 1.0f, 0.0f,
            -1.0f, 1.0f, 0.0f,
    };
    GLuint apos = (GLuint) glGetAttribLocation(program, "aPosition");
    glEnableVertexAttribArray(apos);
    //傳遞頂點
    glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 12, vers);

    //加入材質坐標數據
    static float txts[] = {
            1.0f, 0.0f, //右下
            0.0f, 0.0f,
            1.0f, 1.0f,
            0.0, 1.0
    };
    GLuint atex = (GLuint) glGetAttribLocation(program, "aTexCoord");
    glEnableVertexAttribArray(atex);
    glVertexAttribPointer(atex, 2, GL_FLOAT, GL_FALSE, 8, txts);

    //材質紋理初始化
    //設置紋理層
    glUniform1i(glGetUniformLocation(program, "yTexture"), 0); //對于紋理第1層
    glUniform1i(glGetUniformLocation(program, "uTexture"), 1); //對于紋理第2層
    glUniform1i(glGetUniformLocation(program, "vTexture"), 2); //對于紋理第3層

    //創建opengl紋理
    GLuint texts[3] = {0};
    //創建三個紋理
    glGenTextures(3, texts);

    //設置紋理屬性
    glBindTexture(GL_TEXTURE_2D, texts[0]);
    //縮小的過濾器
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    //設置紋理的格式和大小
    glTexImage2D(GL_TEXTURE_2D,
                 0,           //細節基本 0默認
                 GL_LUMINANCE,//gpu內部格式 亮度,灰度圖
                 width, height, //拉升到全屏
                 0,             //邊框
                 GL_LUMINANCE,//數據的像素格式 亮度,灰度圖 要與上面一致
                 GL_UNSIGNED_BYTE, //像素的數據類型
                 NULL                    //紋理的數據
    );

    //設置紋理屬性
    glBindTexture(GL_TEXTURE_2D, texts[1]);
    //縮小的過濾器
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    //設置紋理的格式和大小
    glTexImage2D(GL_TEXTURE_2D,
                 0,           //細節基本 0默認
                 GL_LUMINANCE,//gpu內部格式 亮度,灰度圖
                 width / 2, height / 2, //拉升到全屏
                 0,             //邊框
                 GL_LUMINANCE,//數據的像素格式 亮度,灰度圖 要與上面一致
                 GL_UNSIGNED_BYTE, //像素的數據類型
                 NULL                    //紋理的數據
    );

    //設置紋理屬性
    glBindTexture(GL_TEXTURE_2D, texts[2]);
    //縮小的過濾器
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    //設置紋理的格式和大小
    glTexImage2D(GL_TEXTURE_2D,
                 0,           //細節基本 0默認
                 GL_LUMINANCE,//gpu內部格式 亮度,灰度圖
                 width / 2, height / 2, //拉升到全屏
                 0,             //邊框
                 GL_LUMINANCE,//數據的像素格式 亮度,灰度圖 要與上面一致
                 GL_UNSIGNED_BYTE, //像素的數據類型
                 NULL                    //紋理的數據
    );


    //////////////////////////////////////////////////////
    ////紋理的修改和顯示
    unsigned char *buf[3] = {0};
    buf[0] = new unsigned char[width * height];
    buf[1] = new unsigned char[width * height / 4];
    buf[2] = new unsigned char[width * height / 4];

5、 傳遞數據
到了這一步,我們的渲染環境算是搭建完畢了,下面就是通過CPU傳遞數據到GPU進行渲染。簡單地說就是獲取在著色器中定義的變量并且對其賦值,然后調用繪制的API即可。
主要代碼:

// 解碼得到YUV數據

                    // 數據Y
                    buf[0] = frame->data[0];

                    memcpy(buf[0],frame->data[0],width*height);
                    // 數據U
                    memcpy(buf[1],frame->data[1],width*height/4);

                    // 數據V
                    memcpy(buf[2],frame->data[2],width*height/4);

                    //激活第1層紋理,綁定到創建的opengl紋理
                    glActiveTexture(GL_TEXTURE0);
                    glBindTexture(GL_TEXTURE_2D,texts[0]);
                    //替換紋理內容
                    glTexSubImage2D(GL_TEXTURE_2D,0,0,0,width,height,GL_LUMINANCE,GL_UNSIGNED_BYTE,buf[0]);


                    //激活第2層紋理,綁定到創建的opengl紋理
                    glActiveTexture(GL_TEXTURE0+1);
                    glBindTexture(GL_TEXTURE_2D,texts[1]);
                    //替換紋理內容
                    glTexSubImage2D(GL_TEXTURE_2D,0,0,0,width/2,height/2,GL_LUMINANCE,GL_UNSIGNED_BYTE,buf[1]);


                    //激活第2層紋理,綁定到創建的opengl紋理
                    glActiveTexture(GL_TEXTURE0+2);
                    glBindTexture(GL_TEXTURE_2D,texts[2]);
                    //替換紋理內容
                    glTexSubImage2D(GL_TEXTURE_2D,0,0,0,width/2,height/2,GL_LUMINANCE,GL_UNSIGNED_BYTE,buf[2]);

                    //三維繪制
                    glDrawArrays(GL_TRIANGLE_STRIP,0,4);
                    //窗口顯示
                    eglSwapBuffers(display,winsurface);

至此,我們的渲染過程就完成了,與前面的ffmpeg解碼為YUV數據聯系起來,實現一邊解碼一邊渲染數據。通過對比筆者很明顯覺察到使用OpenGL渲染的視頻畫面流暢很多。而且對比兩種渲染方式的CPU使用率也發現OpenGL的渲染方式確實使得CPU的使用率大大降低。

最后貼一下結合ffmpeg從解碼到渲染全過程的完整代碼:


#include <jni.h>
#include <string>


#include "FlyLog.h"

#include <android/log.h>
#include <android/native_window.h>
#include <android/native_window_jni.h>

#include <android/native_window_jni.h>
#include <EGL/egl.h>
#include <GLES2/gl2.h>

// 因為ffmpeg是純C代碼,要在cpp中使用則需要使用 extern "C"
extern "C" {
#include "libavutil/avutil.h"

#include <libavformat/avformat.h>

#include <libswscale/swscale.h>

}



//頂點著色器glsl的宏
// 第二個#號的意思是自動鏈接字符串,而不用增加引號,參考ijkplayer的寫法

#define GET_STR(x) #x

static const char *vertexShader = GET_STR(

        attribute  vec4 aPosition; //頂點坐標,在外部獲取傳遞進來

        attribute vec2 aTexCoord; //材質(紋理)頂點坐標

        varying vec2 vTexCoord;   //輸出的材質(紋理)坐標,給片元著色器使用
        void main() {
            //紋理坐標轉換,以左上角為原點的紋理坐標轉換成以左下角為原點的紋理坐標,
            // 比如以左上角為原點的(0,0)對應以左下角為原點的紋理坐標的(0,1)
            vTexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
            gl_Position = aPosition;
        }
);

//片元著色器,軟解碼和部分x86硬解碼解碼得出來的格式是YUV420p

static const char *fragYUV420P = GET_STR(

        precision mediump float;    //精度

        varying vec2 vTexCoord;     //頂點著色器傳遞的坐標,相同名字opengl會自動關聯

        uniform sampler2D yTexture; //輸入的材質(不透明灰度,單像素)

        uniform sampler2D uTexture;

        uniform sampler2D vTexture;
        void main() {
            vec3 yuv;
            vec3 rgb;
            yuv.r = texture2D(yTexture, vTexCoord).r; // y分量
            // 因為UV的默認值是127,所以我們這里要減去0.5(OpenGLES的Shader中會把內存中0~255的整數數值換算為0.0~1.0的浮點數值)
            yuv.g = texture2D(uTexture, vTexCoord).r - 0.5; // u分量
            yuv.b = texture2D(vTexture, vTexCoord).r - 0.5; // v分量
            // yuv轉換成rgb,兩種方法,一種是RGB按照特定換算公式單獨轉換
            // 另外一種是使用矩陣轉換
            rgb = mat3(1.0, 1.0, 1.0,
                       0.0, -0.39465, 2.03211,
                       1.13983, -0.58060, 0.0) * yuv;
            //輸出像素顏色
            gl_FragColor = vec4(rgb, 1.0);
        }
);

GLint InitShader(const char *code, GLint type) {
    //創建shader
    GLint sh = glCreateShader(type);
    if (sh == 0) {
        LOGE("glCreateShader %d failed!", type);
        return 0;
    }
    //加載shader
    glShaderSource(sh,
                   1,    //shader數量
                   &code, //shader代碼
                   0);   //代碼長度
    //編譯shader
    glCompileShader(sh);

    //獲取編譯情況
    GLint status;
    glGetShaderiv(sh, GL_COMPILE_STATUS, &status);
    if (status == 0) {
        LOGE("glCompileShader failed!");
        return 0;
    }
    LOGE("glCompileShader success!");
    return sh;
}


/**
 * 將數據轉換成double類型的一個方法
 * @param r
 * @return
 */
static double r2d(AVRational r) {
    return r.num == 0 || r.den == 0 ? 0 : (double) r.num / (double) r.den;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_flyer_ffmpeg_FlyPlayer_playVideoByOpenGL(JNIEnv *env, jobject thiz, jstring video_path,
                                                  jobject surface) {


    const char *path = env->GetStringUTFChars(video_path, 0);

    AVFormatContext *fmt_ctx;
    // 初始化格式化上下文
    fmt_ctx = avformat_alloc_context();

    // 使用ffmpeg打開文件
    int re = avformat_open_input(&fmt_ctx, path, nullptr, nullptr);
    if (re != 0) {
        LOGE("打開文件失敗:%s", av_err2str(re));
        return;
    }

    //探測流索引
    re = avformat_find_stream_info(fmt_ctx, nullptr);

    if (re < 0) {
        LOGE("索引探測失敗:%s", av_err2str(re));
        return;
    }

    //尋找視頻流索引
    int v_idx = av_find_best_stream(
            fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);

    if (v_idx == -1) {
        LOGE("獲取視頻流索引失敗");
        return;
    }
    //解碼器參數
    AVCodecParameters *c_par;
    //解碼器上下文
    AVCodecContext *cc_ctx;
    //聲明一個解碼器
    const AVCodec *codec;

    c_par = fmt_ctx->streams[v_idx]->codecpar;

    //通過id查找解碼器
    codec = avcodec_find_decoder(c_par->codec_id);

    if (!codec) {

        LOGE("查找解碼器失敗");
        return;
    }

    //用參數c_par實例化編解碼器上下文,,并打開編解碼器
    cc_ctx = avcodec_alloc_context3(codec);

    // 關聯解碼器上下文
    re = avcodec_parameters_to_context(cc_ctx, c_par);

    if (re < 0) {
        LOGE("解碼器上下文關聯失敗:%s", av_err2str(re));
        return;
    }

    //打開解碼器
    re = avcodec_open2(cc_ctx, codec, nullptr);

    if (re != 0) {
        LOGE("打開解碼器失敗:%s", av_err2str(re));
        return;
    }

    // 獲取視頻的寬高,也可以通過解碼器獲取
    AVStream *as = fmt_ctx->streams[v_idx];
    int width = as->codecpar->width;
    int height = as->codecpar->height;

    LOGE("width:%d", width);
    LOGE("height:%d", height);

    //數據包
    AVPacket *pkt;
    //數據幀
    AVFrame *frame;

    //初始化
    pkt = av_packet_alloc();
    frame = av_frame_alloc();

    //1 獲取原始窗口
    ANativeWindow *nwin = ANativeWindow_fromSurface(env, surface);

    ////////////////////
    ///EGL
    //1 EGL display創建和初始化
    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    if (display == EGL_NO_DISPLAY) {
        LOGE("eglGetDisplay failed!");
        return;
    }
    if (EGL_TRUE != eglInitialize(display, 0, 0)) {
        LOGE("eglInitialize failed!");
        return;
    }
    //2 surface
    //2-1 surface窗口配置
    //輸出配置
    EGLConfig config;
    EGLint configNum;
    EGLint configSpec[] = {
            EGL_RED_SIZE, 8,
            EGL_GREEN_SIZE, 8,
            EGL_BLUE_SIZE, 8,
            EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_NONE
    };
    if (EGL_TRUE != eglChooseConfig(display, configSpec, &config, 1, &configNum)) {
        LOGE("eglChooseConfig failed!");
        return;
    }
    //創建surface
    EGLSurface winsurface = eglCreateWindowSurface(display, config, nwin, 0);
    if (winsurface == EGL_NO_SURFACE) {
        LOGE("eglCreateWindowSurface failed!");
        return;
    }

    //3 context 創建關聯的上下文
    const EGLint ctxAttr[] = {
            EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE
    };
    EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctxAttr);
    if (context == EGL_NO_CONTEXT) {
        LOGE("eglCreateContext failed!");
        return;
    }
    if (EGL_TRUE != eglMakeCurrent(display, winsurface, winsurface, context)) {
        LOGE("eglMakeCurrent failed!");
        return;
    }

    LOGE("EGL Init Success!");

    //頂點和片元shader初始化
    //頂點shader初始化
    GLint vsh = InitShader(vertexShader, GL_VERTEX_SHADER);
    //片元yuv420 shader初始化
    GLint fsh = InitShader(fragYUV420P, GL_FRAGMENT_SHADER);


    /////////////////////////////////////////////////////////////
    //創建渲染程序
    GLint program = glCreateProgram();
    if (program == 0) {
        LOGE("glCreateProgram failed!");
        return;
    }
    //渲染程序中加入著色器代碼
    glAttachShader(program, vsh);
    glAttachShader(program, fsh);

    //鏈接程序
    glLinkProgram(program);
    GLint status = 0;
    glGetProgramiv(program, GL_LINK_STATUS, &status);
    if (status != GL_TRUE) {
        LOGE("glLinkProgram failed!");
        return;
    }
    glUseProgram(program);
    LOGE("glLinkProgram success!");
    /////////////////////////////////////////////////////////////


    //加入三維頂點數據 兩個三角形組成正方形
    static float vers[] = {
            1.0f, -1.0f, 0.0f,
            -1.0f, -1.0f, 0.0f,
            1.0f, 1.0f, 0.0f,
            -1.0f, 1.0f, 0.0f,
    };
    GLuint apos = (GLuint) glGetAttribLocation(program, "aPosition");
    glEnableVertexAttribArray(apos);
    //傳遞頂點
    glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 12, vers);

    //加入材質坐標數據
    static float txts[] = {
            1.0f, 0.0f, //右下
            0.0f, 0.0f,
            1.0f, 1.0f,
            0.0, 1.0
    };
    GLuint atex = (GLuint) glGetAttribLocation(program, "aTexCoord");
    glEnableVertexAttribArray(atex);
    glVertexAttribPointer(atex, 2, GL_FLOAT, GL_FALSE, 8, txts);

    //材質紋理初始化
    //設置紋理層
    glUniform1i(glGetUniformLocation(program, "yTexture"), 0); //對于紋理第1層
    glUniform1i(glGetUniformLocation(program, "uTexture"), 1); //對于紋理第2層
    glUniform1i(glGetUniformLocation(program, "vTexture"), 2); //對于紋理第3層

    //創建opengl紋理
    GLuint texts[3] = {0};
    //創建三個紋理
    glGenTextures(3, texts);

    //設置紋理屬性
    glBindTexture(GL_TEXTURE_2D, texts[0]);
    //縮小的過濾器
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    //設置紋理的格式和大小
    glTexImage2D(GL_TEXTURE_2D,
                 0,           //細節基本 0默認
                 GL_LUMINANCE,//gpu內部格式 亮度,灰度圖
                 width, height, //拉升到全屏
                 0,             //邊框
                 GL_LUMINANCE,//數據的像素格式 亮度,灰度圖 要與上面一致
                 GL_UNSIGNED_BYTE, //像素的數據類型
                 NULL                    //紋理的數據
    );

    //設置紋理屬性
    glBindTexture(GL_TEXTURE_2D, texts[1]);
    //縮小的過濾器
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    //設置紋理的格式和大小
    glTexImage2D(GL_TEXTURE_2D,
                 0,           //細節基本 0默認
                 GL_LUMINANCE,//gpu內部格式 亮度,灰度圖
                 width / 2, height / 2, //拉升到全屏
                 0,             //邊框
                 GL_LUMINANCE,//數據的像素格式 亮度,灰度圖 要與上面一致
                 GL_UNSIGNED_BYTE, //像素的數據類型
                 NULL                    //紋理的數據
    );

    //設置紋理屬性
    glBindTexture(GL_TEXTURE_2D, texts[2]);
    //縮小的過濾器
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    //設置紋理的格式和大小
    glTexImage2D(GL_TEXTURE_2D,
                 0,           //細節基本 0默認
                 GL_LUMINANCE,//gpu內部格式 亮度,灰度圖
                 width / 2, height / 2, //拉升到全屏
                 0,             //邊框
                 GL_LUMINANCE,//數據的像素格式 亮度,灰度圖 要與上面一致
                 GL_UNSIGNED_BYTE, //像素的數據類型
                 NULL                    //紋理的數據
    );


    //////////////////////////////////////////////////////
    ////紋理的修改和顯示
    unsigned char *buf[3] = {0};
    buf[0] = new unsigned char[width * height];
    buf[1] = new unsigned char[width * height / 4];
    buf[2] = new unsigned char[width * height / 4];


    while (av_read_frame(fmt_ctx, pkt) >= 0) {//持續讀幀
        // 只解碼視頻流
        if (pkt->stream_index == v_idx) {

            //發送數據包到解碼器
            avcodec_send_packet(cc_ctx, pkt);

            //清理
            av_packet_unref(pkt);

            //這里為什么要使用一個for循環呢?
            // 因為avcodec_send_packet和avcodec_receive_frame并不是一對一的關系的
            //一個avcodec_send_packet可能會出發多個avcodec_receive_frame
            for (;;) {
                // 接受解碼的數據
                re = avcodec_receive_frame(cc_ctx, frame);
                if (re != 0) {
                    break;
                } else {

                    // 解碼得到YUV數據

                    // 數據Y
                    buf[0] = frame->data[0];

                    memcpy(buf[0],frame->data[0],width*height);
                    // 數據U
                    memcpy(buf[1],frame->data[1],width*height/4);

                    // 數據V
                    memcpy(buf[2],frame->data[2],width*height/4);

                    //激活第1層紋理,綁定到創建的opengl紋理
                    glActiveTexture(GL_TEXTURE0);
                    glBindTexture(GL_TEXTURE_2D,texts[0]);
                    //替換紋理內容
                    glTexSubImage2D(GL_TEXTURE_2D,0,0,0,width,height,GL_LUMINANCE,GL_UNSIGNED_BYTE,buf[0]);


                    //激活第2層紋理,綁定到創建的opengl紋理
                    glActiveTexture(GL_TEXTURE0+1);
                    glBindTexture(GL_TEXTURE_2D,texts[1]);
                    //替換紋理內容
                    glTexSubImage2D(GL_TEXTURE_2D,0,0,0,width/2,height/2,GL_LUMINANCE,GL_UNSIGNED_BYTE,buf[1]);


                    //激活第2層紋理,綁定到創建的opengl紋理
                    glActiveTexture(GL_TEXTURE0+2);
                    glBindTexture(GL_TEXTURE_2D,texts[2]);
                    //替換紋理內容
                    glTexSubImage2D(GL_TEXTURE_2D,0,0,0,width/2,height/2,GL_LUMINANCE,GL_UNSIGNED_BYTE,buf[2]);

                    //三維繪制
                    glDrawArrays(GL_TRIANGLE_STRIP,0,4);
                    //窗口顯示
                    eglSwapBuffers(display,winsurface);

                }
            }

        }
    }
    //關閉環境
    avcodec_free_context(&cc_ctx);
    // 釋放資源
    av_frame_free(&frame);
    av_packet_free(&pkt);

    avformat_free_context(fmt_ctx);

    LOGE("播放完畢");

    env->ReleaseStringUTFChars(video_path, path);

}

遇到的問題

筆者測試了兩個不同的視頻發現一個能播放,另外一個花屏,看不出圖像。

查找了一些資料至今仍找不到問題所在,兩個視頻使用SurfaceView渲染都是可以的,說明可能不是解碼的問題,估計是渲染程序的問題。而且兩個視頻解碼出來的YUV數據格式不一樣,一個yuvj420p,這個可以正常使用OpenGL渲染,一個是yuv420p,這個就不能渲染,花屏。

懇親大神不吝賜教。

結束

最后如果你對音視頻開發感興趣可掃碼關注,筆者在各個知識點學習完畢之后也會使用ffmepg從零開始編寫一個多媒體播放器,包括本地播放及網絡流播放等等。歡迎關注,后續我們共同探討,共同進步。


image
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容