基于你的场景(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 端核心难点)
- 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 拷贝,效率低,仅作降级方案)。 - 纹理变换矩阵修正
RTSP 流的视频宽高比可能和屏幕不一致,需根据 frame->width/frame->height 调整 tex_transform 矩阵,避免拉伸;
示例:视频 16:9,屏幕 1:1 时,修正矩阵:
cpp
tex_transform[0] = 9.0f/16.0f; // 水平缩放 - RTSP 流稳定性处理
需添加重连逻辑:av_read_frame 失败时,调用 avformat_close_input 后重新 avformat_open_input;
解码队列缓冲:用队列存储解码后的 AVFrame,避免网络波动导致画面卡顿。 - 权限配置
在 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 视频渲染。
