FFmpeg音视频编解码详解

news2024/11/26 23:16:55

本文你可以了解到

如何在 NDK 层调用 OpenGL ES ,以及使用 OpenGL ES 来渲染 FFmpeg 解码出来的视频数据。

一、渲染流程介绍

Java 层,Android 已经为我们提供了 GLSurfaceView 用于 OpenGL ES 的渲染,我们不必关心 OpenGL ES 中关于 EGL 部分的内容,也无需关注 OpenGL ES 的渲染流程。

NDK 层,就没有那么幸运了,Android 没有为我们提供封装好 OpenGL ES 工具,所以想要使用 OpenGL ES ,一切就只有从头做起了。

下图,是本文整个解码和渲染的流程图。

渲染流程

我们建立了 FFMpeg 解码线程,并且将解码数据输出到本地窗口进行渲染,只用到了一个线程。

而使用 OpenGL ES 来渲染视频,则需要建立另外一个独立线程与 OpenGL ES 进行绑定。

因此,这里涉及到两个线程之间的数据同步问题,这里,我们将 FFmpeg 解码出来的数据送到 绘制器 中,等待 OpenGL ES 线程的调用。

特别说明一下 这里,OpenGL 线程渲染的过程中,不是直接调用绘制器去渲染,而是通过一个代理来间接调用,这样 OpenGL 线程就不需要关心有多少个绘制器需要调用,统统交给代理去管理就好了。

二、创建 OpenGL ES 渲染线程

Java 层一样,先对 EGL 相关的内容进行封装。

EGLCore 封装 EGL 底层操作,如

  • init 初始化
  • eglCreateWindowSurface/eglCreatePbufferSurface 创建渲染表面
  • MakeCurrent 绑定 OpenGL 线程
  • SwapBuffers 交换数据缓冲
  • ......

EGLSurfaceEGLCore 进一步封装,主要是对 EGLCore 创建的 EGLSurface 进行管理,并对外提供更加简洁的调用方法。

本文福利, 免费领取C++音视频学习资料包、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

封装 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);

    // 根据本地窗口创建显示表面
    EGLSurface CreateWindSurface(ANativeWindow *window);

    EGLSurface CreateOffScreenSurface(int width, int height);

    // 将OpenGL上下文和线程进行绑定
    void MakeCurrent(EGLSurface egl_surface);

    // 将缓存数据交换到前台进行显示
    void SwapBuffers(EGLSurface egl_surface);

    // 释放显示
    void DestroySurface(EGLSurface elg_surface);

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

具体实现 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 结束标志
    };

    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 可以既可以创建前台渲染表面,也可以创建离屏渲染表面,离屏渲染主要用于后面合成视频的时候使用。

也就是这两个方法:

EGLSurface CreateWindSurface(ANativeWindow *window);

EGLSurface CreateOffScreenSurface(int width, int height);

创建 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();
};

具体实现 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();
    }
}

创建 OpenGL ES 渲染线程

本文福利, 免费领取C++音视频学习资料包、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

定义成员变量

// opengl_render.h

class OpenGLRender {
private:

    const char *TAG = "OpenGLRender";

    // OpenGL 渲染状态
    enum STATE {
        NO_SURFACE, //没有有效的surface
        FRESH_SURFACE, //持有一个为初始化的新的surface
        RENDERING, //初始化完毕,可以开始渲染
        SURFACE_DESTROY, //surface销毁
        STOP //停止绘制
    };

    JNIEnv *m_env = NULL;

    // 线程依附的JVM环境
    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 相关的成员变量,两个地方说明一下:

一是,定义了渲染线程的状态,我们将根据这几个状态在 OpenGL 线程中做对应的操作。

enum STATE {
    NO_SURFACE, //没有有效的surface
    FRESH_SURFACE, //持有一个未初始化的新的surface
    RENDERING, //初始化完毕,可以开始渲染
    SURFACE_DESTROY, //surface销毁
    STOP //停止绘制
};

二是,这里包含了一个渲染器代理 DrawerProxy ,主要考虑到可能会同时解码多个视频,如果只包含一个绘制器的话,就无法处理了,所以这里将渲染通过代理交给代理者去处理。下一节再详细介绍。

定义成员方法

// opengl_render.h

class OpenGLRender {
private:

    // 省略成员变量...
    
    // 初始化相关的方法
    void InitRenderThread();
    bool InitEGL();
    void InitDspWindow(JNIEnv *env);
    
    // 创建/销毁 Surface
    void CreateSurface();
    void DestroySurface();
    
    // 渲染方法
    void Render();
    
    // 释放资源相关方法
    void ReleaseRender();
    void ReleaseDrawers();
    void ReleaseSurface();
    void ReleaseWindow();
    
    // 渲染线程回调方法
    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();
}

具体实现 opengl_rend.cpp

  • 启动线程
// opengl_render.cpp

OpenGLRender::OpenGLRender(JNIEnv *env, DrawerProxy *drawer_proxy):
m_drawer_proxy(drawer_proxy) {
    this->m_env = env;
    //获取JVM虚拟机,为创建线程作准备
    env->GetJavaVM(&m_jvm_for_thread);
    InitRenderThread();
}

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

void OpenGLRender::InitRenderThread() {
    // 使用智能指针,线程结束时,自动删除本类指针
    std::shared_ptr<OpenGLRender> that(this);
    std::thread t(sRenderThread, that);
    t.detach();
}
  • 线程状态切换
// opengl_render.cpp

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

    //将线程附加到虚拟机,并获取env
    if (that->m_jvm_for_thread->AttachCurrentThread(&env, NULL) != JNI_OK) {
        LOGE(that->TAG, "线程初始化异常");
        return;
    }

    // 初始化 EGL
    if(!that->InitEGL()) {
        //解除线程和jvm关联
        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关联
                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();
}

在进入 while(true) 渲染循环之前,创建了 EglSurface(既上边封装的 EGL 工具), 并调用了它的 Init 方法进行初始化。

进入 while 循环后:

i. 当接收到外部的 SurfaceView 时,将进入 FRESH_SURFACE 状态,这时将对窗口进行初始化,并把窗口绑定给 EGL

ii. 接着,自动进入 RENDERING 状态,开始渲染。

iii. 同时,如果检测到播放退出,进入 STOP 状态,则会释放资源,并退出线程。

  • 设置 SurfaceView ,启动渲染
// 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);

        // 绘制区域的宽高
        m_window_width = ANativeWindow_getWidth(m_native_window);
        m_window_height = ANativeWindow_getHeight(m_native_window);

        //设置宽高限制缓冲区中的像素数量
        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);
}

接着在 CreateSurface 中将窗口绑定给了 EGL

  • 渲染

渲染就很简单了,直接调用渲染代理绘制,再调用 EGLSwapBuffers 交换缓冲数据显示。

// opengl_render.cpp

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

当外部调用 Stop() 方法以后,状态变为 STOP,将会调用 ReleaseRender() ,释放相关资源。

// 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;
    }
}

三、创建 OpenGL ES 绘制器

NDK 层的 OpenGL 绘制过程和 Java 层是一模一样的。代码也尽量从简,主要介绍整体流程

基础绘制器 Drawer

首先将基础操作封装到基类中,这里我们不再详细贴出代码,只看绘制的“骨架”:函数。

头文件 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:
    // 自定义用户数据,可用于存放画面数据
    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);
    
    // 纯虚函数,子类实现
    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;
  }

这里有两个地方重点说明一下,

i. void *cst_data :这个变量用于存放将要绘制的数据,它的类型是 void * ,可以存放任意类型的数据指针,用来存放 FFmpeg 解码好的画面数据。

ii. 最后的几个 virtual 函数,类似 Javaabstract 函数,需要子类实现。

具体实现 drawer.cpp

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

绘制流程和 Java 层的 OpenGL 绘制流程是一样的:

  • 创建纹理ID
  • 创建GL程序
  • 激活、绑定纹理ID
  • 绘制

最后,看下子类的具体实现。

视频绘制器 VideoDrawer

在前面的系列文章中,为了程序的拓展性,定义了渲染器接口 VideoRender 。在视频解码器 VideoDecoder 中,会在完成解码后调用渲染器中的 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;
};

在上文中,虽然我们已经定义了 OpenGLRender 来渲染 OpenGL,但是并没有继承自 VideoRender , 同时前面说过,OpenGLRender 会调用代理渲染器来实现真正的绘制。

因此,这里子类 视频绘制器 VideoDrawer 除了继承 Drawer 以外,还要继承 VideoRender 。具体来看看:

头文件 video_render.h

// video_render.h

class VideoDrawer: public Drawer, public VideoRender {
public:

    VideoDrawer();
    ~VideoDrawer();

    // 实现 VideoRender 定义的方法
    void InitRender(JNIEnv *env, int video_width, int video_height, int *dst_size) override ;
    void Render(OneFrame *one_frame) override ;
    void ReleaseRender() override ;

    // 实现几类定义的方法
    const char* GetVertexShader() override;
    const char* GetFragmentShader() override;
    void InitCstShaderHandler() override;
    void BindTexture() override;
    void PrepareDraw() override;
    void DoneDraw() override;
};

具体实现 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, //纹理内部格式
                     origin_width(), origin_height(), // 画面宽高
                     0, // 必须为0
                     GL_RGBA, // 数据格式,必须和上面的纹理格式保持一直
                     GL_UNSIGNED_BYTE, // RGBA每位数据的字节数,这里是BYTE: 1 byte
                     cst_data);// 画面数据
    }
}

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() {
}

这里最主要的两个方法是:

Render(OneFrame *one_frame) : 将解码好的画面数据并保存到 cst_data 中。

PrepareDraw() : 在绘制前,将 cst_data 中的数据通过 glTexImage2D 方法,映射到 OpenGL2D 纹理中。

绘制代理

前文讲到过,为了兼容多个视频解码渲染的情况,需要定义个代理绘制器,把 Drawer 的调用交给它来实现,下面就来看看如何实现。

定义绘制器代理

// drawer_proxy.h

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

很简单,只有绘制和释放两个外部方法。

实现默认的代理器 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;
};

这里通过一个容器来维护多个绘制器 Drawer

  • 具体实现 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();
}

实现也很简单,将需要绘制的 Drawer 添加到容器中,在 OpenGLRender 调用 Draw() 方法的时候,遍历所有 Drawer ,实现真正的绘制。

四、整合播放

以上,完成了

  • OpenGL 线程的建立
  • EGL 的初始化
  • Drawer 绘制器的定义,VideoDrawer 的建立
  • DrawerProxy 以及 DefDrawerProxyImpl 的定义和实现

最后就差将它们组合到一起,实现整个流程的闭环。

定义 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();
};

实现 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);

    // 创建绘制代理
    DefDrawerProxyImpl *proxyImpl =  new DefDrawerProxyImpl();
    // 将video drawer 注入绘制代理中
    proxyImpl->AddDrawer(m_v_drawer);

    m_v_drawer_proxy = proxyImpl;
    
    // 创建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 中的线程已经使用智能指针,会自动释放相关指针
}

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();
    }
}

在页面中启动播放

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, "视频文件不存在,请在手机根目录下放置 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")
        }
    }
}

本文福利, 免费领取C++音视频学习资料包、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/83631.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

开发游戏相关业务该如何选择云服务器及相关产品?

游戏开发分为两种&#xff0c;第一种就是角色扮演类&#xff0c;另一种就是休闲类游戏&#xff0c;角色扮演类游戏对于计算能力以及游戏安全有很大的尤其&#xff1b;而休闲类游戏对于资源、运维、成本控制有所要求&#xff0c;下面就给大家展示一下腾讯云官方给出的解决方案&a…

如何去做一个完整的网站 SEO 优化方案?

想要做好网站优化&#xff0c;就必须制定一套适合自己的网站优化方案。优化只是一个过程&#xff0c;更多的是简单工作的重复&#xff0c;但也有技巧和方法。这个时候&#xff0c;你的网站优化方案就显得尤为重要。为您指明今后优化工作的途径&#xff0c;您在上一篇文章《传:东…

网络小白入门之路之以太网链路聚合 ---尚文网络奎哥

随着业务的发展和园区网络规模的不断扩大&#xff0c;用户对于网络的带宽、可靠性要求越来越高。传统解决方案通过升级设备方式提高网络带宽&#xff0c;同时通过部署冗余链路并辅以STP&#xff08;Spanning Tree Protocol&#xff0c;生成树协议&#xff09;协议实现高可靠。传…

使用Idea中Docker插件部署并远程Debug

目前在java开发中&#xff0c;由于一套完整的项目所涉及到的微服务模块很多&#xff0c;要是按照传统的方式一个一个部署比较麻烦&#xff0c;所以很多情况下我们都会使用docker镜像的方式进行部署。当我们的应用部署好之后&#xff0c;若运行过程中出现问题&#xff0c;我们也…

docker搭建服务监控 prometheus+node_export+grafana

文章目录下载镜像node-exporter 收集数据prometheus监控搭建grafana数据可视化下载镜像 docker pull grafana/grafana docker pull prom/node-exporter docker pull prom/prometheus链接&#xff1a;点击 提取码&#xff1a;yyds node-exporter 收集数据 docker run -d -p 9…

Android:为了突破瓶颈,你总得新学点什么吧?

一眨眼就到了12月份了&#xff0c;在这拥有“35岁魔咒”IT场上工作多年的你&#xff0c;是否遇到了发展瓶颈&#xff1f;想突破瓶颈有时需要一个机遇&#xff0c;但这个合适的机会很难遇到。这时候&#xff0c;或许你可以思考&#xff0c;自己是否还有改变的空间&#xff1f;如…

不会开赛车的管理者不是好的开发人

今天要讲述的人物&#xff0c;身上的标签比较多元。 他是微软 RD&#xff08; Regional Director &#xff09;兼微软 MVP&#xff1b;在制造业领域深耕十多年&#xff0c;擅长在不同的角色用不同观点看待软件开发流程&#xff0c;热爱探索商业需求和解决方案之间的平衡&#…

目标检测中的不均衡问题综述

导推荐的&#xff0c;简单看了一下&#xff0c;&#xff08;太菜&#xff0c;太多不懂&#xff0c;希望以后会懂&#xff0c;简单做个记录 其实做的是xmind&#xff0c;但是想放到csdn上只能导出成md了。 Imbalance Problems in Object Detection: A Review 类别不平衡 简单…

01GO入门

GO入门一、hello&#xff0c;world二、运行一、hello&#xff0c;world 对上图的说明 1.go文件的后缀是.go 2.package main ​ 表示该hello.go 文件所在的包是main&#xff0c;在go中每个文件都必须归属于一个包。 3.import“fmt” ​ 表示&#xff1a;引入一个包&#xf…

【Docker】Docker镜像是什么?浅谈对Docker镜像的理解

专栏往期文章 《Docker是什么&#xff1f;Docker从介绍到Linux安装图文详细教程》《30条Docker常用命令图文举例总结》 本期目录专栏往期文章1. Docker镜像介绍2. UnionFS介绍3. Docker镜像加载原理4. 为什么Docker镜像要采用分层结构5. 镜像只读, 容器可写1. Docker镜像介绍 …

nodejs+vue社团管理系统

目录 1 绪论 1 1.1 课题背景 1 1.2 课题研究现状 1 1.3 初步设计方法与实施方案 2 1.4 本文研究内容 2 2 系统开发环境 4 开发语言&#xff1a;nodejs 框架&#xff1a;Express 数据库&#xff1a;mysql 数据库工具&#xff1a;Navicat11 开发软件&#x…

前端基础—自动验证

自动验证 在HTML5中&#xff0c;通过对元素使用属性的方法&#xff0c;可以实现在表单提交时执行自动验证的功能。下面是在HTML5中追加的关于对元素内输入内容进行限制的属性的指定。 1&#xff0e;required属性 required属性的主要目的是确保表单控件中的值已填写。在提交时…

#4文献学习总结--能量优化动态计算卸载

文献&#xff1a;“Energy-optimal Dynamic Computation Offloading for Industrial IoT in Fog Computing” 通过将部分计算密集型任务从雾节点动态卸载到云服务器&#xff0c;可以在雾计算系统中进一步改善用户的计算体验。 能量最优动态计算卸载方案&#xff08;EDCO&#…

Spring中@Async注解的使用

一、应用场景 1、同步调用 通常&#xff0c;在Java中的方法调用都是同步调用&#xff0c;比如在A方法中调用了B方法&#xff0c;则在A调用B方法之后&#xff0c;必须等待B方法执行并返回后&#xff0c;A方法才可以继续往下执行。 这样容易出现的一个问题就是如果B方法执行时间…

如何避免“非正常专利申请”?!

近年来&#xff0c;专利数量多但质量不优的现象时而发生。对此&#xff0c;国家知识产权局开始严打非正常申请专利行为。而就在前不久&#xff0c;上海、甘肃等地也出台了相应的地方惩戒措施以打击非正常专利申请&#xff0c;这也反映出未来国家对于专利质量有着更高要求的趋势…

ubuntu 18.04 安装搜狗拼音输入法(没有坑)

本文参考&#xff1a;https://blog.csdn.net/weixin_44497198/article/details/126133691 最近在使用 ubuntu18.04 发现自带的中文输入法太难用了&#xff0c;于是想起装一个搜狗拼音输入法&#xff0c;但是按照搜狗官方的教程安装失败&#xff0c;安装成功了也是不稳定&#x…

钉钉机器人报警设置

钉钉机器人报警设置 1. 钉钉机器人相关设置 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 2. 添加机器人 3. 选择自定义机器人 4. 选择一个安全标签 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 5. 添加完生成一个webhook…

小蓝本 第一本 《因式分解技巧》 第五章 十字相乘 笔记(第五天)

小蓝本 第一本 《因式分解技巧》 第五章 十字相乘 笔记&#xff08;第五天&#xff09;前言十字相乘研究对象类型普通二次三项式基本形式分解步骤注意二次齐次式基本形式分组步骤注意系数和为0的普通二次三项式习题5题目题解前言 今天的干货来了&#xff0c;十字相乘。 十字相…

【JS】事件基础

JavaScript事件基础事件的概述事件三要素常见的事件事件的调用在script标签中调用在元素中调用鼠标事件onclick事件onmouseover和onmouseoutonmousedown和onmouseup页面事件onloadonbeforeunloadthis其他事件事件的概述 事件操作是JavaScript的核心。 用户进行操作时&#xff0…

疫情在家用Python搞副业,也能月入10000+

下班副业实现经济自由的时候&#xff0c;你还在床上躺着&#xff0c;天天摆烂吗&#xff1f;这样的生活真的是你想要的吗&#xff1f; 疫情在家接一些Python相关的小单子&#xff0c;既能给自己练手&#xff0c;还能赚是真香 从零基础开始真的一台电脑和一部手机就可以✅ 一…