【Android 音視頻開發(fā)打怪升級(jí):FFmpeg音視頻編解碼篇】五跌榔、Android FFmpeg+OpenGL ES播放視頻

聲 明

首先异雁,這一系列文章均基于自己的理解和實(shí)踐,可能有不對(duì)的地方僧须,歡迎大家指正纲刀。
其次,這是一個(gè)入門系列担平,涉及的知識(shí)也僅限于夠用示绊,深入的知識(shí)網(wǎng)上也有許許多多的博文供大家學(xué)習(xí)了锭部。
最后,寫文章過程中面褐,會(huì)借鑒參考其他人分享的文章拌禾,會(huì)在文章最后列出,感謝這些作者的分享展哭。

碼字不易湃窍,轉(zhuǎn)載請(qǐng)注明出處!

教程代碼:【Github傳送門

目錄

一匪傍、Android音視頻硬解碼篇:

二秧饮、使用OpenGL渲染視頻畫面篇

三、Android FFmpeg音視頻解碼篇

本文你可以了解到

如何在 NDK 層調(diào)用 OpenGL ES 灵再,以及使用 OpenGL ES 來渲染 FFmpeg 解碼出來的視頻數(shù)據(jù)。

一亿笤、渲染流程介紹

Java 層翎迁,Android 已經(jīng)為我們提供了 GLSurfaceView 用于 OpenGL ES 的渲染,我們不必關(guān)心 OpenGL ES 中關(guān)于 EGL 部分的內(nèi)容净薛,也無需關(guān)注 OpenGL ES 的渲染流程汪榔。

NDK 層,就沒有那么幸運(yùn)了肃拜,Android 沒有為我們提供封裝好 OpenGL ES 工具揍异,所以想要使用 OpenGL ES 全陨,一切就只有從頭做起了。

但是也不必?fù)?dān)心衷掷,關(guān)于 EGL 的使用辱姨,在前面文章【深入了解OpenGL之EGL】中專門做了詳細(xì)的介紹,在 NDK 層戚嗅,也是一樣的雨涛,不過是使用 C/C++ 實(shí)現(xiàn)一遍而已。

下圖懦胞,是本文整個(gè)解碼和渲染的流程圖替久。

渲染流程

在【Android FFmpeg視頻解碼播放】中,我們建立了 FFMpeg 解碼線程躏尉,并且將解碼數(shù)據(jù)輸出到本地窗口進(jìn)行渲染蚯根,只用到了一個(gè)線程。

而使用 OpenGL ES 來渲染視頻胀糜,則需要建立另外一個(gè)獨(dú)立線程與 OpenGL ES 進(jìn)行綁定颅拦。

因此,這里涉及到兩個(gè)線程之間的數(shù)據(jù)同步問題教藻,這里距帅,我們將 FFmpeg 解碼出來的數(shù)據(jù)送到 繪制器 中,等待 OpenGL ES 線程的調(diào)用括堤。

特別說明一下
這里碌秸,OpenGL 線程渲染的過程中,不是直接調(diào)用繪制器去渲染悄窃,而是通過一個(gè)代理來間接調(diào)用讥电,這樣 OpenGL 線程就不需要關(guān)心有多少個(gè)繪制器需要調(diào)用,統(tǒng)統(tǒng)交給代理去管理就好了轧抗。

二允趟、創(chuàng)建 OpenGL ES 渲染線程

Java 層一樣,先對(duì) EGL 相關(guān)的內(nèi)容進(jìn)行封裝鸦致。

EGLCore 封裝 EGL 底層操作潮剪,如

  • init 初始化
  • eglCreateWindowSurface/eglCreatePbufferSurface 創(chuàng)建渲染表面
  • MakeCurrent 綁定 OpenGL 線程
  • SwapBuffers 交換數(shù)據(jù)緩沖
  • ......

EGLSurface 對(duì) EGLCore 進(jìn)一步封裝,主要是對(duì) EGLCore 創(chuàng)建的 EGLSurface 進(jìn)行管理分唾,并對(duì)外提供更加簡(jiǎn)潔的調(diào)用方法抗碰。

EGL 原理請(qǐng)閱讀《深入了解OpenGL之EGL》一文,這里將不再具體介紹绽乔。

封裝 EGLCore

頭文件 elg_core.h

// egl_core.h

extern "C" {
#include <EGL/egl.h>
#include <EGL/eglext.h>
};

class EglCore {
private:
    const char *TAG = "EglCore";

    // EGL顯示窗口
    EGLDisplay m_egl_dsp = EGL_NO_DISPLAY;
    // EGL上線問
    EGLContext m_egl_cxt = EGL_NO_CONTEXT;
    // EGL配置
    EGLConfig m_egl_cfg;

    EGLConfig GetEGLConfig();

public:
    EglCore();
    ~EglCore();

    bool Init(EGLContext share_ctx);

    // 根據(jù)本地窗口創(chuàng)建顯示表面
    EGLSurface CreateWindSurface(ANativeWindow *window);

    EGLSurface CreateOffScreenSurface(int width, int height);

    // 將OpenGL上下文和線程進(jìn)行綁定
    void MakeCurrent(EGLSurface egl_surface);

    // 將緩存數(shù)據(jù)交換到前臺(tái)進(jìn)行顯示
    void SwapBuffers(EGLSurface egl_surface);

    // 釋放顯示
    void DestroySurface(EGLSurface elg_surface);

    // 釋放ELG
    void Release();
};

具體實(shí)現(xiàn) egl_core.cpp

// egl_core.cpp

bool EglCore::Init(EGLContext share_ctx) {
    if (m_egl_dsp != EGL_NO_DISPLAY) {
        LOGE(TAG, "EGL already set up")
        return true;
    }

    if (share_ctx == NULL) {
        share_ctx = EGL_NO_CONTEXT;
    }

    m_egl_dsp = eglGetDisplay(EGL_DEFAULT_DISPLAY);

    if (m_egl_dsp == EGL_NO_DISPLAY || eglGetError() != EGL_SUCCESS) {
        LOGE(TAG, "EGL init display fail")
        return false;
    }

    EGLint major_ver, minor_ver;
    EGLBoolean success = eglInitialize(m_egl_dsp, &major_ver, &minor_ver);
    if (success != EGL_TRUE || eglGetError() != EGL_SUCCESS) {
        LOGE(TAG, "EGL init fail")
        return false;
    }

    LOGI(TAG, "EGL version: %d.%d", major_ver, minor_ver)

    m_egl_cfg = GetEGLConfig();

    const EGLint attr[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
    m_egl_cxt = eglCreateContext(m_egl_dsp, m_egl_cfg, share_ctx, attr);
    if (m_egl_cxt == EGL_NO_CONTEXT) {
        LOGE(TAG, "EGL create fail, error is %x", eglGetError());
        return false;
    }

    EGLint egl_format;
    success = eglGetConfigAttrib(m_egl_dsp, m_egl_cfg, EGL_NATIVE_VISUAL_ID, &egl_format);
    if (success != EGL_TRUE || eglGetError() != EGL_SUCCESS) {
        LOGE(TAG, "EGL get config fail")
        return false;
    }

    LOGI(TAG, "EGL init success")
    return true;
}

EGLConfig EglCore::GetEGLConfig() {
    EGLint numConfigs;
    EGLConfig config;
    
    static const EGLint CONFIG_ATTRIBS[] = {
          EGL_BUFFER_SIZE, EGL_DONT_CARE,
          EGL_RED_SIZE, 8,
          EGL_GREEN_SIZE, 8,
          EGL_BLUE_SIZE, 8,
          EGL_ALPHA_SIZE, 8,
          EGL_DEPTH_SIZE, 16,
          EGL_STENCIL_SIZE, EGL_DONT_CARE,
          EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
          EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
          EGL_NONE // the end 結(jié)束標(biāo)志
    };

    EGLBoolean success = eglChooseConfig(m_egl_dsp, CONFIG_ATTRIBS, &config, 1, &numConfigs);
    if (!success || eglGetError() != EGL_SUCCESS) {
        LOGE(TAG, "EGL config fail")
        return NULL;
    }
    return config;
}

EGLSurface EglCore::CreateWindSurface(ANativeWindow *window) {
    EGLSurface surface = eglCreateWindowSurface(m_egl_dsp, m_egl_cfg, window, 0);
    if (eglGetError() != EGL_SUCCESS) {
        LOGI(TAG, "EGL create window surface fail")
        return NULL;
    }
    return surface;
}

EGLSurface EglCore::CreateOffScreenSurface(int width, int height) {
    int CONFIG_ATTRIBS[] = {
            EGL_WIDTH, width,
            EGL_HEIGHT, height,
            EGL_NONE
    };

    EGLSurface surface = eglCreatePbufferSurface(m_egl_dsp, m_egl_cfg, CONFIG_ATTRIBS);
    if (eglGetError() != EGL_SUCCESS) {
        LOGI(TAG, "EGL create off screen surface fail")
        return NULL;
    }
    return surface;
}

void EglCore::MakeCurrent(EGLSurface egl_surface) {
    if (!eglMakeCurrent(m_egl_dsp, egl_surface, egl_surface, m_egl_cxt)) {
        LOGE(TAG, "EGL make current fail");
    }
}

void EglCore::SwapBuffers(EGLSurface egl_surface) {
    eglSwapBuffers(m_egl_dsp, egl_surface);
}

void EglCore::DestroySurface(EGLSurface elg_surface) {
    eglMakeCurrent(m_egl_dsp, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
    eglDestroySurface(m_egl_dsp, elg_surface);
}

void EglCore::Release() {
    if (m_egl_dsp != EGL_NO_DISPLAY) {
        eglMakeCurrent(m_egl_dsp, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
        eglDestroyContext(m_egl_dsp, m_egl_cxt);
        eglReleaseThread();
        eglTerminate(m_egl_dsp);
    }
    m_egl_dsp = EGL_NO_DISPLAY;
    m_egl_cxt = EGL_NO_CONTEXT;
    m_egl_cfg = NULL;
}

說明一下弧蝇,EGL 可以既可以創(chuàng)建前臺(tái)渲染表面,也可以創(chuàng)建離屏渲染表面,離屏渲染主要用于后面合成視頻的時(shí)候使用看疗。

也就是這兩個(gè)方法:

EGLSurface CreateWindSurface(ANativeWindow *window);

EGLSurface CreateOffScreenSurface(int width, int height);

創(chuàng)建 EglSurface

頭文件 egl_surface.h

// egl_surface.h

#include <android/native_window.h>
#include "egl_core.h"

class EglSurface {
private:

    const char *TAG = "EglSurface";

    ANativeWindow *m_native_window = NULL;

    EglCore *m_core;

    EGLSurface m_surface;

public:
    EglSurface();
    ~EglSurface();
    
    bool Init();
    void CreateEglSurface(ANativeWindow *native_window, int width, int height);
    void MakeCurrent();
    void SwapBuffers();
    void DestroyEglSurface();
    void Release();
};

具體實(shí)現(xiàn) egl_surface.cpp

// egl_surface.cpp

EglSurface::EglSurface() {
    m_core = new EglCore();
}

EglSurface::~EglSurface() {
    delete m_core;
}

bool EglSurface::Init() {
    return m_core->Init(NULL);
}

void EglSurface::CreateEglSurface(ANativeWindow *native_window,
                                  int width, int height) {
    if (native_window != NULL) {
        this->m_native_window = native_window;
        m_surface = m_core->CreateWindSurface(m_native_window);
    } else {
        m_surface = m_core->CreateOffScreenSurface(width, height);
    }
    if (m_surface == NULL) {
        LOGE(TAG, "EGL create window surface fail")
        Release();
    }
    MakeCurrent();
}

void EglSurface::SwapBuffers() {
    m_core->SwapBuffers(m_surface);
}

void EglSurface::MakeCurrent() {
    m_core->MakeCurrent(m_surface);
}

void EglSurface::DestroyEglSurface() {
    if (m_surface != NULL) {
        if (m_core != NULL) {
            m_core->DestroySurface(m_surface);
        }
        m_surface = NULL;
    }
}

void EglSurface::Release() {
    DestroyEglSurface();
    if (m_core != NULL) {
        m_core->Release();
    }
}

創(chuàng)建 OpenGL ES 渲染線程

定義成員變量

// opengl_render.h

class OpenGLRender {
private:

    const char *TAG = "OpenGLRender";

    // OpenGL 渲染狀態(tài)
    enum STATE {
        NO_SURFACE, //沒有有效的surface
        FRESH_SURFACE, //持有一個(gè)為初始化的新的surface
        RENDERING, //初始化完畢沙峻,可以開始渲染
        SURFACE_DESTROY, //surface銷毀
        STOP //停止繪制
    };

    JNIEnv *m_env = NULL;

    // 線程依附的JVM環(huán)境
    JavaVM *m_jvm_for_thread = NULL;

    // Surface引用,必須使用引用两芳,否則無法在線程中操作
    jobject m_surface_ref = NULL;

    // 本地屏幕
    ANativeWindow *m_native_window = NULL;

    // EGL顯示表面
    EglSurface *m_egl_surface = NULL;

    // 繪制代理器
    DrawerProxy *m_drawer_proxy = NULL;

    int m_window_width = 0;
    int m_window_height = 0;

    STATE m_state = NO_SURFACE;
    
    // 省略其他...
}

除了定義 EGL 相關(guān)的成員變量摔寨,兩個(gè)地方說明一下:

一是,定義了渲染線程的狀態(tài)怖辆,我們將根據(jù)這幾個(gè)狀態(tài)在 OpenGL 線程中做對(duì)應(yīng)的操作是复。

enum STATE {
    NO_SURFACE, //沒有有效的surface
    FRESH_SURFACE, //持有一個(gè)未初始化的新的surface
    RENDERING, //初始化完畢,可以開始渲染
    SURFACE_DESTROY, //surface銷毀
    STOP //停止繪制
};

二是竖螃,這里包含了一個(gè)渲染器代理 DrawerProxy 淑廊,主要考慮到可能會(huì)同時(shí)解碼多個(gè)視頻,如果只包含一個(gè)繪制器的話特咆,就無法處理了季惩,所以這里將渲染通過代理交給代理者去處理。下一節(jié)再詳細(xì)介紹腻格。

定義成員方法

// opengl_render.h

class OpenGLRender {
private:

    // 省略成員變量...
    
    // 初始化相關(guān)的方法
    void InitRenderThread();
    bool InitEGL();
    void InitDspWindow(JNIEnv *env);
    
    // 創(chuàng)建/銷毀 Surface
    void CreateSurface();
    void DestroySurface();
    
    // 渲染方法
    void Render();
    
    // 釋放資源相關(guān)方法
    void ReleaseRender();
    void ReleaseDrawers();
    void ReleaseSurface();
    void ReleaseWindow();
    
    // 渲染線程回調(diào)方法
    static void sRenderThread(std::shared_ptr<OpenGLRender> that);

public:
    OpenGLRender(JNIEnv *env, DrawerProxy *drawer_proxy);
    ~OpenGLRender();

    void SetSurface(jobject surface);
    void SetOffScreenSize(int width, int height);
    void Stop();
}

具體實(shí)現(xiàn) opengl_rend.cpp

  • 啟動(dòng)線程
// opengl_render.cpp

OpenGLRender::OpenGLRender(JNIEnv *env, DrawerProxy *drawer_proxy):
m_drawer_proxy(drawer_proxy) {
    this->m_env = env;
    //獲取JVM虛擬機(jī)画拾,為創(chuàng)建線程作準(zhǔn)備
    env->GetJavaVM(&m_jvm_for_thread);
    InitRenderThread();
}

OpenGLRender::~OpenGLRender() {
    delete m_egl_surface;
}

void OpenGLRender::InitRenderThread() {
    // 使用智能指針,線程結(jié)束時(shí)荒叶,自動(dòng)刪除本類指針
    std::shared_ptr<OpenGLRender> that(this);
    std::thread t(sRenderThread, that);
    t.detach();
}
  • 線程狀態(tài)切換
// opengl_render.cpp

void OpenGLRender::sRenderThread(std::shared_ptr<OpenGLRender> that) {
    JNIEnv * env;

    //將線程附加到虛擬機(jī)碾阁,并獲取env
    if (that->m_jvm_for_thread->AttachCurrentThread(&env, NULL) != JNI_OK) {
        LOGE(that->TAG, "線程初始化異常");
        return;
    }

    // 初始化 EGL
    if(!that->InitEGL()) {
        //解除線程和jvm關(guān)聯(lián)
        that->m_jvm_for_thread->DetachCurrentThread();
        return;
    }

    while (true) {
        switch (that->m_state) {
            case FRESH_SURFACE:
                LOGI(that->TAG, "Loop Render FRESH_SURFACE")
                that->InitDspWindow(env);
                that->CreateSurface();
                that->m_state = RENDERING;
                break;
            case RENDERING:
                that->Render();
                break;
            case SURFACE_DESTROY:
                LOGI(that->TAG, "Loop Render SURFACE_DESTROY")
                that->DestroySurface();
                that->m_state = NO_SURFACE;
                break;
            case STOP:
                LOGI(that->TAG, "Loop Render STOP")
                //解除線程和jvm關(guān)聯(lián)
                that->ReleaseRender();
                that->m_jvm_for_thread->DetachCurrentThread();
                return;
            case NO_SURFACE:
            default:
                break;
        }
        usleep(20000);
    }
}

bool OpenGLRender::InitEGL() {
    m_egl_surface = new EglSurface();
    return m_egl_surface->Init();
}

在進(jìn)入 while(true) 渲染循環(huán)之前输虱,創(chuàng)建了 EglSurface(既上邊封裝的 EGL 工具)些楣, 并調(diào)用了它的 Init 方法進(jìn)行初始化。

進(jìn)入 while 循環(huán)后:

i. 當(dāng)接收到外部的 SurfaceView 時(shí)宪睹,將進(jìn)入 FRESH_SURFACE 狀態(tài)愁茁,這時(shí)將對(duì)窗口進(jìn)行初始化,并把窗口綁定給 EGL 亭病。

ii. 接著鹅很,自動(dòng)進(jìn)入 RENDERING 狀態(tài),開始渲染罪帖。

iii. 同時(shí)促煮,如果檢測(cè)到播放退出,進(jìn)入 STOP 狀態(tài)整袁,則會(huì)釋放資源菠齿,并退出線程。

  • 設(shè)置 SurfaceView 坐昙,啟動(dòng)渲染
// opengl_render.cpp

void OpenGLRender::SetSurface(jobject surface) {
    if (NULL != surface) {
        m_surface_ref = m_env->NewGlobalRef(surface);
        m_state = FRESH_SURFACE;
    } else {
        m_env->DeleteGlobalRef(m_surface_ref);
        m_state = SURFACE_DESTROY;
    }
}

void OpenGLRender::InitDspWindow(JNIEnv *env) {
    if (m_surface_ref != NULL) {
        // 初始化窗口
        m_native_window = ANativeWindow_fromSurface(env, m_surface_ref);

        // 繪制區(qū)域的寬高
        m_window_width = ANativeWindow_getWidth(m_native_window);
        m_window_height = ANativeWindow_getHeight(m_native_window);

        //設(shè)置寬高限制緩沖區(qū)中的像素?cái)?shù)量
        ANativeWindow_setBuffersGeometry(m_native_window, m_window_width,
                                         m_window_height, WINDOW_FORMAT_RGBA_8888);

        LOGD(TAG, "View Port width: %d, height: %d", m_window_width, m_window_height)
    }
}

void OpenGLRender::CreateSurface() {
    m_egl_surface->CreateEglSurface(m_native_window, m_window_width, m_window_height);
    glViewport(0, 0, m_window_width, m_window_height);
}

可以看到绳匀,ANativeWindow 窗口的初始化和《Android FFmpeg視頻解碼播放》中直接使用本地窗口顯示視頻畫面時(shí)一樣的。

接著在 CreateSurface 中將窗口綁定給了 EGL

  • 渲染

渲染就很簡(jiǎn)單了疾棵,直接調(diào)用渲染代理繪制戈钢,再調(diào)用 EGLSwapBuffers 交換緩沖數(shù)據(jù)顯示。

// opengl_render.cpp

void OpenGLRender::Render() {
    if (RENDERING == m_state) {
        m_drawer_proxy->Draw();
        m_egl_surface->SwapBuffers();
    }
}
  • 釋放資源

當(dāng)外部調(diào)用 Stop() 方法以后是尔,狀態(tài)變?yōu)?STOP殉了,將會(huì)調(diào)用 ReleaseRender() ,釋放相關(guān)資源嗜历。

// opengl_render.cpp

void OpenGLRender::Stop() {
    m_state = STOP;
}

void OpenGLRender::ReleaseRender() {
    ReleaseDrawers();
    ReleaseSurface();
    ReleaseWindow();
}

void OpenGLRender::ReleaseSurface() {
    if (m_egl_surface != NULL) {
        m_egl_surface->Release();
        delete m_egl_surface;
        m_egl_surface = NULL;
    }
}

void OpenGLRender::ReleaseWindow() {
    if (m_native_window != NULL) {
        ANativeWindow_release(m_native_window);
        m_native_window = NULL;
    }
}

void OpenGLRender::ReleaseDrawers() {
    if (m_drawer_proxy != NULL) {
        m_drawer_proxy->Release();
        delete m_drawer_proxy;
        m_drawer_proxy = NULL;
    }
}

三宣渗、創(chuàng)建 OpenGL ES 繪制器

NDK 層的 OpenGL 繪制過程和 Java 層是一模一樣的,所以將不再贅述這個(gè)過程了梨州,具體請(qǐng)見《初步了解OpenGL ES》和《使用OpenGL渲染視頻畫面》痕囱。代碼也盡量從簡(jiǎn),主要介紹整體流程暴匠,具體代碼可查看【Demo 源碼的 draw 】鞍恢。

基礎(chǔ)繪制器 Drawer

首先將基礎(chǔ)操作封裝到基類中,這里我們不再詳細(xì)貼出代碼每窖,只看繪制的“骨架”:函數(shù)帮掉。

頭文件 drawer.h

// drawer.h
class Drawer {
private:
    // 省略成員變量...
    
    void CreateTextureId();
    void CreateProgram();
    GLuint LoadShader(GLenum type, const GLchar *shader_code);
    void DoDraw();
    
public:

    void Draw();

    bool IsReadyToDraw();
    
    void Release();

protected:
    // 自定義用戶數(shù)據(jù),可用于存放畫面數(shù)據(jù)
    void *cst_data = NULL;

    void SetSize(int width, int height);
    void ActivateTexture(GLenum type = GL_TEXTURE_2D, GLuint texture = m_texture_id,
                         GLenum index = 0, int texture_handler = m_texture_handler);
    
    // 純虛函數(shù)窒典,子類實(shí)現(xiàn)
    virtual const char* GetVertexShader() = 0;
    virtual const char* GetFragmentShader() = 0;
    virtual void InitCstShaderHandler() = 0;
    virtual void BindTexture() = 0;
    virtual void PrepareDraw() = 0;
    virtual void DoneDraw() = 0;
  }

這里有兩個(gè)地方重點(diǎn)說明一下蟆炊,

i. void *cst_data :這個(gè)變量用于存放將要繪制的數(shù)據(jù),它的類型是 void * 瀑志,可以存放任意類型的數(shù)據(jù)指針涩搓,用來存放 FFmpeg 解碼好的畫面數(shù)據(jù)。

ii. 最后的幾個(gè) virtual 函數(shù)劈猪,類似 Javaabstract 函數(shù)昧甘,需要子類實(shí)現(xiàn)。

具體實(shí)現(xiàn) drawer.cpp

主要看 Draw() 方法战得,詳細(xì)請(qǐng)看【源碼

// drawer.cpp
void Drawer::Draw() {
    if (IsReadyToDraw()) {
        CreateTextureId();
        CreateProgram();
        BindTexture();
        PrepareDraw();
        DoDraw();
        DoneDraw();
    }
}

繪制流程和 Java 層的 OpenGL 繪制流程是一樣的:

  • 創(chuàng)建紋理ID
  • 創(chuàng)建GL程序
  • 激活充边、綁定紋理ID
  • 繪制

最后,看下子類的具體實(shí)現(xiàn)常侦。

視頻繪制器 VideoDrawer

在前面的系列文章中浇冰,為了程序的拓展性,定義了渲染器接口 VideoRender 聋亡。在視頻解碼器 VideoDecoder 中肘习,會(huì)在完成解碼后調(diào)用渲染器中的 Render() 方法。

class VideoRender {
public:
    virtual void InitRender(JNIEnv *env, int video_width, int video_height, int *dst_size) = 0;
    virtual void Render(OneFrame *one_frame) = 0;
    virtual void ReleaseRender() = 0;
};

在上文中杀捻,雖然我們已經(jīng)定義了 OpenGLRender 來渲染 OpenGL井厌,但是并沒有繼承自 VideoRender 蚓庭, 同時(shí)前面說過,OpenGLRender 會(huì)調(diào)用代理渲染器來實(shí)現(xiàn)真正的繪制仅仆。

因此器赞,這里子類 視頻繪制器 VideoDrawer 除了繼承 Drawer 以外,還要繼承 VideoRender 墓拜。具體來看看:

頭文件 video_render.h

// video_render.h

class VideoDrawer: public Drawer, public VideoRender {
public:

    VideoDrawer();
    ~VideoDrawer();

    // 實(shí)現(xiàn) VideoRender 定義的方法
    void InitRender(JNIEnv *env, int video_width, int video_height, int *dst_size) override ;
    void Render(OneFrame *one_frame) override ;
    void ReleaseRender() override ;

    // 實(shí)現(xiàn)幾類定義的方法
    const char* GetVertexShader() override;
    const char* GetFragmentShader() override;
    void InitCstShaderHandler() override;
    void BindTexture() override;
    void PrepareDraw() override;
    void DoneDraw() override;
};

具體實(shí)現(xiàn) video_render.cpp

// video_render.cpp

VideoDrawer::VideoDrawer(): Drawer(0, 0) {
}

VideoDrawer::~VideoDrawer() {

}

void VideoDrawer::InitRender(JNIEnv *env, int video_width, int video_height, int *dst_size) {
    SetSize(video_width, video_height);
    dst_size[0] = video_width;
    dst_size[1] = video_height;
}

void VideoDrawer::Render(OneFrame *one_frame) {
    cst_data = one_frame->data;
}

void VideoDrawer::BindTexture() {
    ActivateTexture();
}

void VideoDrawer::PrepareDraw() {
    if (cst_data != NULL) {
        glTexImage2D(GL_TEXTURE_2D, 0, // level一般為0
                     GL_RGBA, //紋理內(nèi)部格式
                     origin_width(), origin_height(), // 畫面寬高
                     0, // 必須為0
                     GL_RGBA, // 數(shù)據(jù)格式港柜,必須和上面的紋理格式保持一直
                     GL_UNSIGNED_BYTE, // RGBA每位數(shù)據(jù)的字節(jié)數(shù),這里是BYTE: 1 byte
                     cst_data);// 畫面數(shù)據(jù)
    }
}

const char* VideoDrawer::GetVertexShader() {
    const GLbyte shader[] = "attribute vec4 aPosition;\n"
                            "attribute vec2 aCoordinate;\n"
                            "varying vec2 vCoordinate;\n"
                            "void main() {\n"
                            "  gl_Position = aPosition;\n"
                            "  vCoordinate = aCoordinate;\n"
                            "}";
    return (char *)shader;
}

const char* VideoDrawer::GetFragmentShader() {
    const GLbyte shader[] = "precision mediump float;\n"
                            "uniform sampler2D uTexture;\n"
                            "varying vec2 vCoordinate;\n"
                            "void main() {\n"
                            "  vec4 color = texture2D(uTexture, vCoordinate);\n"
                            "  gl_FragColor = color;\n"
                            "}";
    return (char *)shader;
}

void VideoDrawer::ReleaseRender() {
}

void VideoDrawer::InitCstShaderHandler() {

}

void VideoDrawer::DoneDraw() {
}

這里最主要的兩個(gè)方法是:

Render(OneFrame *one_frame) : 將解碼好的畫面數(shù)據(jù)并保存到 cst_data 中咳榜。

PrepareDraw() : 在繪制前夏醉,將 cst_data 中的數(shù)據(jù)通過 glTexImage2D 方法,映射到 OpenGL2D 紋理中涌韩。

繪制代理

前文講到過畔柔,為了兼容多個(gè)視頻解碼渲染的情況,需要定義個(gè)代理繪制器臣樱,把 Drawer 的調(diào)用交給它來實(shí)現(xiàn)靶擦,下面就來看看如何實(shí)現(xiàn)。

定義繪制器代理

// drawer_proxy.h

class DrawerProxy {
public:
    virtual void Draw() = 0;
    virtual void Release() = 0;
    virtual ~DrawerProxy() {}
};

很簡(jiǎn)單雇毫,只有繪制和釋放兩個(gè)外部方法衷旅。

實(shí)現(xiàn)默認(rèn)的代理器 DefDrawerProxyImpl

  • 頭文件 def_drawer_proxy_impl.h
// def_drawer_proxy_impl.h

class DefDrawerProxyImpl: public DrawerProxy {

private:
    std::vector<Drawer *> m_drawers;

public:
    void AddDrawer(Drawer *drawer);
    void Draw() override;
    void Release() override;
};

這里通過一個(gè)容器來維護(hù)多個(gè)繪制器 Drawer 苗胀。

  • 具體實(shí)現(xiàn) def_drawer_proxy_impl.cpp
// def_drawer_proxy_impl.cpp

void DefDrawerProxyImpl::AddDrawer(Drawer *drawer) {
    m_drawers.push_back(drawer);
}

void DefDrawerProxyImpl::Draw() {
    for (int i = 0; i < m_drawers.size(); ++i) {
        m_drawers[i]->Draw();為初始化
    }
}

void DefDrawerProxyImpl::Release() {
    for (int i = 0; i < m_drawers.size(); ++i) {
        m_drawers[i]->Release();
        delete m_drawers[i];
    }

    m_drawers.clear();
}

實(shí)現(xiàn)也很簡(jiǎn)單,將需要繪制的 Drawer 添加到容器中宇整,在 OpenGLRender 調(diào)用
Draw() 方法的時(shí)候蝗羊,遍歷所有 Drawer 落剪,實(shí)現(xiàn)真正的繪制耸采。

四艳吠、整合播放

以上,完成了

  • OpenGL 線程的建立
  • EGL 的初始化
  • Drawer 繪制器的定義孝冒,VideoDrawer 的建立
  • DrawerProxy 以及 DefDrawerProxyImpl 的定義和實(shí)現(xiàn)

最后就差將它們組合到一起柬姚,實(shí)現(xiàn)整個(gè)流程的閉環(huán)拟杉。

定義 GLPlayer

頭文件 gl_player.h

// gl_player.h

class GLPlayer {

private:
    VideoDecoder *m_v_decoder;
    OpenGLRender *m_gl_render;

    DrawerProxy *m_v_drawer_proxy;
    VideoDrawer *m_v_drawer;

    AudioDecoder *m_a_decoder;
    AudioRender *m_a_render;

public:
    GLPlayer(JNIEnv *jniEnv, jstring path);
    ~GLPlayer();

    void SetSurface(jobject surface);
    void PlayOrPause();
    void Release();
};

實(shí)現(xiàn) gl_player.cpp


GLPlayer::GLPlayer(JNIEnv *jniEnv, jstring path) {
    m_v_decoder = new VideoDecoder(jniEnv, path);

    // OpenGL 渲染
    m_v_drawer = new VideoDrawer();
    m_v_decoder->SetRender(m_v_drawer);

    // 創(chuàng)建繪制代理
    DefDrawerProxyImpl *proxyImpl =  new DefDrawerProxyImpl();
    // 將video drawer 注入繪制代理中
    proxyImpl->AddDrawer(m_v_drawer);

    m_v_drawer_proxy = proxyImpl;
    
    // 創(chuàng)建OpenGL繪制器
    m_gl_render = new OpenGLRender(jniEnv, m_v_drawer_proxy);

    // 音頻解碼
    m_a_decoder = new AudioDecoder(jniEnv, path, false);
    m_a_render = new OpenSLRender();
    m_a_decoder->SetRender(m_a_render);
}

GLPlayer::~GLPlayer() {
    // 此處不需要 delete 成員指針
    // 在BaseDecoder 和 OpenGLRender 中的線程已經(jīng)使用智能指針庄涡,會(huì)自動(dòng)釋放相關(guān)指針
}

void GLPlayer::SetSurface(jobject surface) {
    m_gl_render->SetSurface(surface);
}

void GLPlayer::PlayOrPause() {
    if (!m_v_decoder->IsRunning()) {
        m_v_decoder->GoOn();
    } else {
        m_v_decoder->Pause();
    }
    if (!m_a_decoder->IsRunning()) {
        m_a_decoder->GoOn();
    } else {
        m_a_decoder->Pause();
    }
}

void GLPlayer::Release() {
    m_gl_render->Stop();
    m_v_decoder->Stop();
    m_a_decoder->Stop();
}

定義 JNI 接口

// native-lib.cpp

extern "C" {
    JNIEXPORT jint JNICALL
    Java_com_cxp_learningvideo_FFmpegGLPlayerActivity_createGLPlayer(
        JNIEnv *env,
        jobject  /* this */,
        jstring path,
        jobject surface) {
        
        GLPlayer *player = new GLPlayer(env, path);
        player->SetSurface(surface);
        return (jint) player;
    }

    JNIEXPORT void JNICALL
    Java_com_cxp_learningvideo_FFmpegGLPlayerActivity_playOrPause(
        JNIEnv *env,
        jobject  /* this */,
        jint player) {
        
        GLPlayer *p = (GLPlayer *) player;
        p->PlayOrPause();
    }


    JNIEXPORT void JNICALL
    Java_com_cxp_learningvideo_FFmpegGLPlayerActivity_stop(
        JNIEnv *env,
        jobject  /* this */,
        jint player) {
        
        GLPlayer *p = (GLPlayer *) player;
        p->Release();
    }
}

在頁面中啟動(dòng)播放

class FFmpegGLPlayerActivity: AppCompatActivity() {

    val path = Environment.getExternalStorageDirectory().absolutePath + "/mvtest.mp4"

    private var player: Int? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ff_gl_player)
        initSfv()
    }

    private fun initSfv() {
        if (File(path).exists()) {
            sfv.holder.addCallback(object : SurfaceHolder.Callback {
                override fun surfaceChanged(holder: SurfaceHolder, format: Int,
                                            width: Int, height: Int) {}
                override fun surfaceDestroyed(holder: SurfaceHolder) {
                    stop(player!!)
                }

                override fun surfaceCreated(holder: SurfaceHolder) {
                    if (player == null) {
                        player = createGLPlayer(path, holder.surface)
                        playOrPause(player!!)
                    }
                }
            })
        } else {
            Toast.makeText(this, "視頻文件不存在,請(qǐng)?jiān)谑謾C(jī)根目錄下放置 mvtest.mp4", Toast.LENGTH_SHORT).show()
        }
    }

    private external fun createGLPlayer(path: String, surface: Surface): Int
    private external fun playOrPause(player: Int)
    private external fun stop(player: Int)

    companion object {
        init {
            System.loadLibrary("native-lib")
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載搬设,如需轉(zhuǎn)載請(qǐng)通過簡(jiǎn)信或評(píng)論聯(lián)系作者穴店。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市拿穴,隨后出現(xiàn)的幾起案子泣洞,更是在濱河造成了極大的恐慌,老刑警劉巖默色,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件球凰,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)呕诉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門缘厢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人甩挫,你說我怎么就攤上這事贴硫。” “怎么了伊者?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵英遭,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我亦渗,道長(zhǎng)挖诸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任法精,我火速辦了婚禮税灌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘亿虽。我一直安慰自己菱涤,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布洛勉。 她就那樣靜靜地躺著粘秆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪收毫。 梳的紋絲不亂的頭發(fā)上攻走,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音此再,去河邊找鬼昔搂。 笑死,一個(gè)胖子當(dāng)著我的面吹牛输拇,可吹牛的內(nèi)容都是我干的摘符。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼策吠,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼逛裤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起猴抹,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤带族,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蟀给,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蝙砌,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡阳堕,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了择克。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嘱丢。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖祠饺,靈堂內(nèi)的尸體忽然破棺而出越驻,到底是詐尸還是另有隱情,我是刑警寧澤道偷,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布缀旁,位于F島的核電站,受9級(jí)特大地震影響勺鸦,放射性物質(zhì)發(fā)生泄漏并巍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一换途、第九天 我趴在偏房一處隱蔽的房頂上張望懊渡。 院中可真熱鬧,春花似錦军拟、人聲如沸剃执。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肾档。三九已至,卻和暖如春辫继,著一層夾襖步出監(jiān)牢的瞬間怒见,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工姑宽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留遣耍,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓炮车,卻偏偏與公主長(zhǎng)得像舵变,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子示血,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344