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 视频渲染。

相关推荐
祖国的好青年1 小时前
VS Code 搭建 React Native 开发环境(Windows 实战指南)
android·windows·react native·react.js
黄林晴1 小时前
警惕!AGP 9.2 别只改版本号,R8 规则与构建链路全线收紧
android·gradle
小米渣的逆袭2 小时前
Android ADB 完全使用指南
android·adb
儿歌八万首2 小时前
Jetpack Compose Canvas 进阶:结合 animateFloatAsState 让自定义图形动起来
android·动画·compose
zhangphil3 小时前
Android Page 3 Flow读sql数据库媒体文件,Kotlin
android·kotlin
神探小白牙3 小时前
echarts,3d堆叠图
android·3d·echarts
李白的天不白3 小时前
如何项目发布到github上
android·vue.js
summerkissyou19873 小时前
Android-RTC、NTP 和 System Time(系统时间)
android
小书房3 小时前
Kotlin使用体验及理解1
android·开发语言·kotlin
撩得Android一次心动4 小时前
Android Navigation 组件全面讲解
android·jetpack·navigation