opengl 播放视频的android c++ 方案

基于你的场景(android_main 纯 C/C++ 开发,无法用 Java 绑定 SurfaceTexture,需拉取 RTSP 流并生成视频纹理),核心方案是:用 FFmpeg + OpenGL ES 外部纹理(GL_TEXTURE_EXTERNAL_OES)实现纯 C 端 RTSP 流解码 + 渲染,无需 Java 层介入,完全适配 NativeActivity 架构。

以下是完整流程(含 RTSP 拉流、FFmpeg 硬件解码、外部纹理绑定、渲染闭环):

核心思路

RTSP 拉流:用 FFmpeg avformat_open_input 打开 RTSP 流,获取视频流信息;

硬件解码:用 FFmpeg avcodec_find_decoder 找到硬件解码器(如 AV_CODEC_ID_H264),解码后输出 AVFrame(优先获取 GPU 端解码帧,避免拷贝);

外部纹理绑定:将 FFmpeg 解码后的 GPU 帧(如 Android ANativeWindowBuffer)绑定到 GL_TEXTURE_EXTERNAL_OES 纹理;

渲染循环:每帧同步纹理数据,用之前的平面几何体渲染视频。

前置依赖

需编译 FFmpeg 为 Android 平台静态库(启用 --enable-hardcoded-tables --enable-neon --enable-jni --enable-mediacodec 等选项,支持硬件解码),并链接 EGL/OpenGL ES 库。

完整流程(纯 C 代码,适配 android_main)

步骤 1:定义全局变量(适配 NativeActivity 生命周期)

cpp

cpp 复制代码
#include <jni.h>
#include <android/native_activity.h>
#include <EGL/egl.h>
#include <GLES3/gl3.h>
#include <GLES3/gl3ext.h>
#include <ffmpeg/avformat.h>
#include <ffmpeg/avcodec.h>
#include <ffmpeg/swscale.h>
#include <ffmpeg/avutil.h>

// EGL 环境(NativeActivity 需手动初始化)
EGLDisplay eglDisplay = EGL_NO_DISPLAY;
EGLSurface eglSurface = EGL_NO_SURFACE;
EGLContext eglContext = EGL_NO_CONTEXT;

// 视频相关
AVFormatContext* fmt_ctx = NULL;
AVCodecContext* codec_ctx = NULL;
int video_stream_idx = -1;
AVFrame* frame = NULL;
AVPacket* pkt = NULL;

// OpenGL 资源
GLuint vao, vbo, ebo;
GLuint program;
GLuint video_tex_id; // 外部纹理 ID(GL_TEXTURE_EXTERNAL_OES)
GLint mvp_loc, tex_transform_loc, video_tex_loc;

// 纹理变换矩阵(修正视频拉伸/翻转)

float tex_transform[16] = {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};

步骤 2:EGL 环境初始化(NativeActivity 必须手动创建)

cpp

cpp 复制代码
// 初始化 EGL 上下文(适配 NativeActivity 的 ANativeWindow)
bool init_egl(ANativeWindow* window) {
    // 1. 获取 EGL 显示
    eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    eglInitialize(eglDisplay, NULL, NULL);

    // 2. 配置 EGL 像素格式
    const EGLint config_attrs[] = {
        EGL_RED_SIZE, 8,
        EGL_GREEN_SIZE, 8,
        EGL_BLUE_SIZE, 8,
        EGL_ALPHA_SIZE, 8,
        EGL_DEPTH_SIZE, 16,
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
        EGL_NONE
    };
    EGLConfig config;
    EGLint config_count;
    eglChooseConfig(eglDisplay, config_attrs, &config, 1, &config_count);

    // 3. 创建 EGL Surface(绑定 ANativeWindow)
    eglSurface = eglCreateWindowSurface(eglDisplay, config, window, NULL);

    // 4. 创建 EGL Context
    const EGLint ctx_attrs[] = {
        EGL_CONTEXT_CLIENT_VERSION, 3,
        EGL_NONE
    };
    eglContext = eglCreateContext(eglDisplay, config, EGL_NO_CONTEXT, ctx_attrs);

    // 5. 绑定上下文
    eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);
    return true;
}

步骤 3:初始化平面几何体(VAO/VBO/EBO,复用之前逻辑)

cpp

cpp 复制代码
void init_geometry() {
    float vertices[] = {
        -1.0f, 1.0f, 0.0f,  0.0f, 1.0f,
        -1.0f, -1.0f, 0.0f, 0.0f, 0.0f,
        1.0f, -1.0f, 0.0f,  1.0f, 0.0f,
        1.0f, 1.0f, 0.0f,   1.0f, 1.0f
    };
    uint32_t indices[] = {0,1,2, 0,2,3};

    glGenVertexArrays(1, &vao);
    glGenBuffers(1, &vbo);
    glGenBuffers(1, &ebo);

    glBindVertexArray(vao);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    // 位置属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5*sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    // 纹理坐标属性
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5*sizeof(float), (void*)(3*sizeof(float)));
    glEnableVertexAttribArray(1);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

步骤 4:初始化外部纹理(GL_TEXTURE_EXTERNAL_OES)

cpp

cpp 复制代码
bool init_video_texture() {
    // 检查外部纹理扩展
    const char* extensions = (const char*)glGetString(GL_EXTENSIONS);
    if (!strstr(extensions, "GL_OES_EGL_image_external")) {
        LOGD("不支持 GL_OES_EGL_image_external 扩展");
        return false;
    }

    // 生成外部纹理
    glGenTextures(1, &video_tex_id);
    glBindTexture(GL_TEXTURE_EXTERNAL_OES, video_tex_id);
    // 配置纹理参数(外部纹理限制)
    glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0);
    return true;
}

步骤 5:FFmpeg 初始化(RTSP 拉流 + 硬件解码)

cpp

cpp 复制代码
bool init_ffmpeg(const char* rtsp_url) {
    // 1. 注册 FFmpeg 组件
    avformat_network_init();
    av_register_all();

    // 2. 打开 RTSP 流
    if (avformat_open_input(&fmt_ctx, rtsp_url, NULL, NULL) != 0) {
        LOGD("无法打开 RTSP 流: %s", rtsp_url);
        return false;
    }

    // 3. 查找流信息
    if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {
        LOGD("无法获取流信息");
        return false;
    }

    // 4. 查找视频流索引和解码器
    for (int i = 0; i < fmt_ctx->nb_streams; i++) {
        if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_stream_idx = i;
            // 查找硬件解码器(优先用 MediaCodec 硬件解码)
            AVCodec* codec = avcodec_find_decoder_by_name("h264_mediacodec");
            if (!codec) {
                codec = avcodec_find_decoder(fmt_ctx->streams[i]->codecpar->codec_id);
                LOGD("使用软件解码器");
            } else {
                LOGD("使用硬件解码器 h264_mediacodec");
            }
            // 创建解码器上下文
            codec_ctx = avcodec_alloc_context3(codec);
            avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[i]->codecpar);
            // 打开解码器
            if (avcodec_open2(codec_ctx, codec, NULL) != 0) {
                LOGD("无法打开解码器");
                return false;
            }
            break;
        }
    }
    if (video_stream_idx == -1) {
        LOGD("未找到视频流");
        return false;
    }

    // 5. 初始化帧和数据包
    frame = av_frame_alloc();
    pkt = av_packet_alloc();
    return true;
}

步骤 6:编译视频着色器(纯 C 字符串,无需文件)

cpp

cpp 复制代码
GLuint create_shader_program() {
    // 顶点着色器源码
    const char* vert_source = R"(
        #version 300 es
        layout (location = 0) in vec3 aPos;
        layout (location = 1) in vec2 aTexCoord;
        out vec2 vTexCoord;
        uniform mat4 uMvpMatrix;
        uniform mat4 uTexTransform;

        void main() {
            gl_Position = uMvpMatrix * vec4(aPos, 1.0);
            vTexCoord = (uTexTransform * vec4(aTexCoord, 0.0, 1.0)).xy;
        }
    )";

    // 片段着色器源码(外部纹理专用)
    const char* frag_source = R"(
        #version 300 es
        #extension GL_OES_EGL_image_external : require
        precision mediump float;
        in vec2 vTexCoord;
        uniform samplerExternalOES uVideoTexture;
        out vec4 fragColor;

        void main() {
            fragColor = texture(uVideoTexture, vTexCoord);
        }
    )";

    // 编译顶点着色器
    GLuint vert_shader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vert_shader, 1, &vert_source, NULL);
    glCompileShader(vert_shader);

    // 编译片段着色器
    GLuint frag_shader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(frag_shader, 1, &frag_source, NULL);
    glCompileShader(frag_shader);

    // 链接程序
    GLuint program = glCreateProgram();
    glAttachShader(program, vert_shader);
    glAttachShader(program, frag_shader);
    glLinkProgram(program);

    // 获取 uniform 位置
    mvp_loc = glGetUniformLocation(program, "uMvpMatrix");
    tex_transform_loc = glGetUniformLocation(program, "uTexTransform");
    video_tex_loc = glGetUniformLocation(program, "uVideoTexture");

    // 释放中间产物
    glDeleteShader(vert_shader);
    glDeleteShader(frag_shader);
    return program;
}

步骤 7:核心渲染循环(RTSP 帧同步 + 渲染)

cpp

cpp 复制代码
void render_loop() {
    // MVP 矩阵(正交投影,适配屏幕)
    float mvp[16] = {
        1.0f, 0.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f, 0.0f,
        0.0f, 0.0f, -1.0f, 0.0f,
        0.0f, 0.0f, 0.0f, 1.0f
    };

    while (1) {
        // 1. 读取 RTSP 数据包并解码
        if (av_read_frame(fmt_ctx, pkt) == 0) {
            if (pkt->stream_index == video_stream_idx) {
                // 发送数据包到解码器
                avcodec_send_packet(codec_ctx, pkt);
                // 接收解码后的帧
                if (avcodec_receive_frame(codec_ctx, frame) == 0) {
                    // 2. 同步帧到外部纹理(关键:将 FFmpeg 帧绑定到 GL_TEXTURE_EXTERNAL_OES)
                    // 这里分两种情况:硬件解码(GPU 帧)和软件解码(CPU 帧)
                    if (frame->format == AV_PIX_FMT_MEDIACODEC) {
                        // 硬件解码:frame->data[3] 是 ANativeWindowBuffer 指针(Android 特有)
                        ANativeWindowBuffer* buf = (ANativeWindowBuffer*)frame->data[3];
                        // 将 ANativeWindowBuffer 绑定到外部纹理(需用 EGLImage 桥接)
                        EGLImageKHR egl_image = eglCreateImageKHR(
                            eglDisplay, eglContext, EGL_NATIVE_BUFFER_ANDROID,
                            (EGLClientBuffer)buf, NULL
                        );
                        glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, (GLeglImageOES)egl_image);
                        eglDestroyImageKHR(eglDisplay, egl_image);
                    } else {
                        // 软件解码:需将 CPU 帧上传到外部纹理(效率较低,不推荐)
                        glBindTexture(GL_TEXTURE_EXTERNAL_OES, video_tex_id);
                        glTexSubImage2D(
                            GL_TEXTURE_EXTERNAL_OES, 0, 0, 0,
                            frame->width, frame->height,
                            GL_RGBA, GL_UNSIGNED_BYTE, frame->data[0]
                        );
                        glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0);
                    }
                }
            }
            av_packet_unref(pkt);
        }

        // 3. 清屏
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // 4. 激活着色器并传递参数
        glUseProgram(program);
        glUniformMatrix4fv(mvp_loc, 1, GL_FALSE, mvp);
        glUniformMatrix4fv(tex_transform_loc, 1, GL_FALSE, tex_transform);

        // 5. 绑定外部纹理
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_EXTERNAL_OES, video_tex_id);
        glUniform1i(video_tex_loc, 0);

        // 6. 绘制平面
        glBindVertexArray(vao);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);

        // 7. 解绑资源
        glBindVertexArray(0);
        glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0);
        glUseProgram(0);

        // 8. 交换缓冲区显示
        eglSwapBuffers(eglDisplay, eglSurface);

        // 9. 延迟,避免CPU占用过高
        usleep(16000); // 约60fps
    }
}

步骤 8:NativeActivity 生命周期回调(android_main 入口)

cpp

cpp 复制代码
void android_main(struct android_app* app) {
    // 1. 初始化 ANativeWindow
    ANativeActivity* activity = app->activity;
    ANativeWindow* window = activity->window;

    // 2. 初始化 EGL
    if (!init_egl(window)) {
        LOGD("EGL 初始化失败");
        return;
    }

    // 3. 初始化 OpenGL 资源
    init_geometry();
    if (!init_video_texture()) {
        LOGD("视频纹理初始化失败");
        return;
    }
    program = create_shader_program();

    // 4. 初始化 FFmpeg 并打开 RTSP 流(替换为你的 RTSP 地址)
    if (!init_ffmpeg("rtsp://example.com/stream")) {
        LOGD("FFmpeg 初始化失败");
        return;
    }

    // 5. 启动渲染循环
    render_loop();

    // 6. 释放资源(简化,实际需处理退出逻辑)
    av_frame_free(&frame);
    av_packet_free(&pkt);
    avcodec_close(codec_ctx);
    avcodec_free_context(&codec_ctx);
    avformat_close_input(&fmt_ctx);
    avformat_network_deinit();

    glDeleteVertexArrays(1, &vao);
    glDeleteBuffers(1, &vbo);
    glDeleteBuffers(1, &ebo);
    glDeleteTextures(1, &video_tex_id);
    glDeleteProgram(program);

    eglDestroyContext(eglDisplay, eglContext);
    eglDestroySurface(eglDisplay, eglSurface);
    eglTerminate(eglDisplay);
}

关键说明(纯 C 端核心难点)

  1. FFmpeg 硬件解码帧绑定外部纹理
    当 FFmpeg 使用 Android MediaCodec 硬件解码时,AVFrame->format 为 AV_PIX_FMT_MEDIACODEC,frame->data[3] 存储 ANativeWindowBuffer 指针(GPU 端缓冲区);
    需通过 eglCreateImageKHR 将 ANativeWindowBuffer 转为 EGLImage,再用 glEGLImageTargetTexture2DOES 绑定到 GL_TEXTURE_EXTERNAL_OES 纹理(零拷贝,效率最高);
    软件解码帧需用 glTexSubImage2D 上传到纹理(CPU→GPU 拷贝,效率低,仅作降级方案)。
  2. 纹理变换矩阵修正
    RTSP 流的视频宽高比可能和屏幕不一致,需根据 frame->width/frame->height 调整 tex_transform 矩阵,避免拉伸;
    示例:视频 16:9,屏幕 1:1 时,修正矩阵:
    cpp
    tex_transform[0] = 9.0f/16.0f; // 水平缩放
  3. RTSP 流稳定性处理
    需添加重连逻辑:av_read_frame 失败时,调用 avformat_close_input 后重新 avformat_open_input;
    解码队列缓冲:用队列存储解码后的 AVFrame,避免网络波动导致画面卡顿。
  4. 权限配置
    在 AndroidManifest.xml 中添加网络权限(RTSP 需网络):
    xml
cpp 复制代码
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:glEsVersion="0x00030000" android:required="true" />

适配 XR 场景的扩展

VR 双屏渲染:为左眼 / 右眼分别创建 MVP 矩阵(用 OpenXR 的 XrMatrix4x4f_CreateProjectionFov 生成),在渲染循环中绘制两次平面,分别绑定左右眼纹理;

360° 全景视频:将平面几何体替换为天空盒(立方体 / 球面),纹理改为立方体贴图格式的外部纹理,着色器适配 samplerCubeOES 采样。

常见问题排查

RTSP 无法连接:检查网络权限、RTSP 地址正确性、防火墙是否拦截;

硬件解码失败:FFmpeg 编译时未启用 mediacodec,需重新编译 FFmpeg 并添加 --enable-mediacodec;

画面黑屏:外部纹理扩展未支持、EGLImage 创建失败、着色器编译错误(用 glGetShaderInfoLog 查看错误日志);

画面卡顿:网络波动(添加缓冲队列)、CPU 占用过高(优先用硬件解码)。

这套方案完全基于 C/C++ 实现,无需 Java 层介入,完美适配 android_main 架构,可直接用于 NativeActivity 或 XR Native 项目的 RTSP 视频渲染。

相关推荐
abner.Li1 小时前
android 反编译
android
Digitally1 小时前
如何删除 realme 手机上的短信
android
2501_916008891 小时前
提高 iOS 应用逆向难度的工程实践,多工具联动的全栈安全方案
android·安全·ios·小程序·uni-app·cocoa·iphone
沐怡旸1 小时前
【底层机制】Android图形渲染体系深度解析:VSync信号机制
android·面试
BoomHe2 小时前
Android 13 (API 33)上自定义广播
android
来来走走3 小时前
Android开发(Kotlin) ViewModel基本用法
android·开发语言·kotlin
Digitally3 小时前
在荣耀手机上删除短信的完整指南
android
nono牛4 小时前
Android Binder C/C++ 层详解与实践
android·c语言·binder
Coder-coco4 小时前
个人健康系统|健康管理|基于java+Android+微信小程序的个人健康系统设计与实现(源码+数据库+文档)
android·java·vue.js·spring boot·微信小程序·论文·个人健康系统