ExoPlayer GL demo 深度解析:用 GLSurfaceView + GL Shader 实现视频位图叠加渲染
2026/9/20 12:53:40 网站建设 项目流程

ExoPlayer GL demo 深度解析:用 GLSurfaceView + GL Shader 实现视频位图叠加渲染

【免费下载链接】ExoPlayerAn extensible media player for Android项目地址: https://gitcode.com/gh_mirrors/exop/ExoPlayer

本文围绕 ExoPlayer 仓库中的demos/gl示例应用展开,讲解如何将解码后的视频帧渲染到GLSurfaceView上,并通过 GL shader 对画面进行后处理。该 demo 在视频画面上叠加了一张由 Android Canvas 动态绘制的位图(包含应用图标与当前帧的展示时间戳),完整示范了"ExoPlayer 视频输出 → SurfaceTexture → GL 纹理 → 自定义着色器 → 屏幕"这一核心渲染链路。读完本文,你将掌握在 ExoPlayer 上接入自定义 GL 视频处理器的接口设计、着色器编写方法、帧时间戳获取技巧,以及受保护内容(DRM)场景下安全 GL 上下文的创建方式。

Demo 定位与核心能力

关联文档 demos/gl/README.md 对该 demo 的描述非常精炼:它演示了如何将视频渲染到 [GLSurfaceView] 并应用一个 GL shader。shader 的作用是在视频顶部叠加一个位图,该位图使用 Android Canvas 绘制,其中包含当前帧的展示时间戳(presentation timestamp),用于展示如何从离屏 SurfaceTexture 中获取当前帧的时间戳。

虽然 README 正文只有寥寥数行,但其指向的实现代码是整个仓库中"自定义 GL 视频渲染"最完整的参考实现之一,共包含 4 个 Java 文件与 2 个 GLSL shader:

文件作用
MainActivity.java播放器初始化、MediaSource 构建、把播放器挂到 GL 视图上
VideoProcessingGLSurfaceView.java核心:GLSurfaceView 子类,管理 EGL 上下文、SurfaceTexture、帧时间戳对齐,定义VideoProcessor处理接口
BitmapOverlayVideoProcessor.java用 Android Canvas 绘制叠加位图并上传为 GL 纹理,运行 shader 完成合成
package-info.java包文档

两个 shader 位于 demos/gl/src/main/assets/,顶点着色器负责顶点与纹理坐标变换,片元着色器负责视频与叠加位图的 alpha 混合。

构建与运行

该 demo 是 Gradle 项目demo-gl,在根目录 settings.gradle 中注册:

include modulePrefix + 'demo-gl' project(modulePrefix + 'demo-gl').projectDir = new File(rootDir, 'demos/gl')

构建与运行方式遵循 demos/README.md 的通用流程:

  • Android Studio:File → New → Import Project 导入 ExoPlayer 根目录,从运行配置下拉框中选择demo-gl运行;
  • 命令行 Gradle:在仓库根目录执行./gradlew :demo-gl:tasks查看可用任务,从Install tasks中选择安装任务,例如./gradlew :demo-gl:installDebug

安装启动后,MainActivity 默认播放远程 MP4 测试片源:

private static final String DEFAULT_MEDIA_URI = "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv";

同时,应用在 AndroidManifest.xml 中声明了com.google.android.exoplayer.gldemo.action.VIEW的 intent-filter,支持httphttpscontentassetfile五种 scheme,因此也可以通过隐式 Intent 播放任意视频地址,例如:

adb shell am start -a com.google.android.exoplayer.gldemo.action.VIEW -d "https://example.com/video.mp4"

Intent 还支持三个可选 extra:extension(强制指定媒体类型扩展名)、drm_scheme(如cenc)、drm_license_url(DRM License 服务地址),详见下文。

渲染链路总览:ExoPlayer 如何把帧交给 GL

整个 demo 的架构核心是 VideoProcessingGLSurfaceView.java。它与普通GLSurfaceView用法的区别在于:视频解码输出不再直接交给系统 Surface,而是交给一个离屏SurfaceTexture,GL 渲染线程再把它当作外部纹理(samplerExternalOES)采样、加工并绘制到窗口。

布局:双视图叠加

main_activity.xml 中,StyledPlayerView设置了app:surface_type="none",即播放器不创建自己的 Surface;随后 MainActivity 把VideoProcessingGLSurfaceView以代码方式添加到R.id.exo_content_frameStyledPlayerView预留的内容容器)中,形成"控制 UI 在播放器视图层、视频画面在 GL 视图层"的叠加结构。

播放器接线

MainActivity.initializePlayer() 做了三件关键的事:

  1. 根据 Intent 与Util.inferContentType判断内容类型,只支持 DASH(DashMediaSource)与普通渐进式文件(ProgressiveMediaSource)两类;
  2. 构建ExoPlayer,设置循环播放REPEAT_MODE_ALL
  3. 将同一个 player 同时挂到StyledPlayerViewVideoProcessingGLSurfaceView
videoProcessingGLSurfaceView.setPlayer(player); playerView.setPlayer(player); player.addAnalyticsListener(new EventLogger());

其中setPlayer()内部(VideoProcessingGLSurfaceView.java)分别调用了player.setVideoSurface(surface)把解码输出导向SurfaceTexture对应的 Surface,以及player.setVideoFrameMetadataListener(renderer)让渲染器在每帧即将渲染时拿到时间戳与格式信息。

帧到达驱动渲染

VideoRenderer(GLSurfaceView 的内部 Renderer)在onSurfaceCreated时创建外部纹理与SurfaceTexture,并注册OnFrameAvailableListener——每收到一帧就把frameAvailable原子标志置位并调用requestRender()(VideoProcessingGLSurfaceView.java)。视图使用setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY),即仅当新帧到达时才触发渲染,避免空转耗电。

onDrawFrame中(VideoProcessingGLSurfaceView.java)完成核心四步:

  1. 首次绘制时调用videoProcessor.initialize()初始化 shader 程序与纹理;
  2. 有尺寸变化时调用videoProcessor.setSurfaceSize(width, height)
  3. 有新帧时调用surfaceTexture.updateTexImage()更新外部纹理,并通过getTimestamp()获取 SurfaceTexture 侧的时间戳,再用sampleTimestampQueue.poll(lastFrameTimestampNs)反查出该帧的展示时间戳,最后getTransformMatrix取纹理坐标变换矩阵;
  4. 调用videoProcessor.draw(texture, frameTimestampUs, transformMatrix)让处理器完成实际的 GL 绘制。

VideoProcessor:可插拔的视频处理接口

为了让"渲染后处理"逻辑与视图解耦,VideoProcessingGLSurfaceView.java 定义了一个内部接口:

public interface VideoProcessor { /** Performs any required GL initialization. */ void initialize(); /** Sets the size of the output surface in pixels. */ void setSurfaceSize(int width, int height); /** * Draws using GL operations. * * @param frameTexture The ID of a GL texture containing a video frame. * @param frameTimestampUs The presentation timestamp of the frame, in microseconds. * @param transformMatrix The 4 * 4 transform matrix to be applied to the texture. */ void draw(int frameTexture, long frameTimestampUs, float[] transformMatrix); /** Releases any resources associated with this {@link VideoProcessor}. */ void release(); }

四个方法职责清晰:

  • initialize():在 GL 线程上创建 shader program、生成纹理,只能做 GL 相关初始化;
  • setSurfaceSize():接收视口像素尺寸,处理器可据此计算缩放比例等参数;
  • draw():每帧调用的核心方法,入参是视频帧的外部纹理 ID、该帧展示时间戳(微秒)以及 4×4 纹理变换矩阵;
  • release():释放 shader 与纹理资源。

这个抽象意味着:要自定义效果(如滤镜、缩放、水印、转场),只需实现新的VideoProcessor并传入VideoProcessingGLSurfaceView构造器,视图本身完全不需要改动。

BitmapOverlayVideoProcessor:Canvas 位图叠加的完整实现

BitmapOverlayVideoProcessor.java 是该接口的参考实现,演示"用 Android Canvas 画一张位图 → 上传为 GL 纹理 → 与视频帧混合"的全过程。

准备阶段:Canvas 位图

构造函数创建了一块 512×256 的ARGB_8888位图与配套 Canvas,并从应用包信息中取出应用图标:

private static final int OVERLAY_WIDTH = 512; private static final int OVERLAY_HEIGHT = 256; overlayBitmap = Bitmap.createBitmap(OVERLAY_WIDTH, OVERLAY_HEIGHT, Bitmap.Config.ARGB_8888); overlayCanvas = new Canvas(overlayBitmap); logoBitmap = ((BitmapDrawable) context.getPackageManager().getApplicationIcon(context.getPackageName())).getBitmap();

initialize():shader 与纹理

initialize()用 ExoPlayer 的 GlProgram 工具类加载 assets 中的两个 shader 文件,绑定顶点与纹理坐标缓冲(使用GlUtil.getNormalizedCoordinateBounds()GlUtil.getTextureCoordinateBounds(),坐标向量大小为GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE,即 4 分量齐次坐标),随后生成一个GL_TEXTURE_2D纹理用于承载叠加位图,并设置GL_NEAREST/GL_LINEAR过滤与GL_REPEAT环绕方式。

draw():每帧动态更新叠加内容

每帧draw()里先重绘位图——擦除为透明、画上应用图标、再画上格式化后的当前帧时间戳字符串:

String text = String.format(Locale.US, "%.02f", frameTimestampUs / (float) C.MICROS_PER_SECOND); overlayBitmap.eraseColor(Color.TRANSPARENT); overlayCanvas.drawBitmap(logoBitmap, /* left= */ 32, /* top= */ 32, paint); overlayCanvas.drawText(text, /* x= */ 200, /* y= */ 130, paint); GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]); GLUtils.texSubImage2D(GL10.GL_TEXTURE_2D, 0, 0, 0, overlayBitmap);

然后绑定 uniform 并绘制全屏三角形带:

program.setSamplerTexIdUniform("uTexSampler0", frameTexture, /* texUnitIndex= */ 0); program.setSamplerTexIdUniform("uTexSampler1", textures[0], /* texUnitIndex= */ 1); program.setFloatUniform("uScaleX", bitmapScaleX); program.setFloatUniform("uScaleY", bitmapScaleY); program.setFloatsUniform("uTexTransform", transformMatrix); program.bindAttributesAndUniforms(); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);

其中bitmapScaleX/YsetSurfaceSize中按视口尺寸与位图尺寸之比计算,用于片元着色器中把位图纹理坐标缩放适配到屏幕。

着色器详解

顶点着色器

bitmap_overlay_video_processor_vertex.glsl 很简洁:把顶点位置直接输出到gl_Position,用uTexTransform(即 SurfaceTexture 提供的变换矩阵)变换纹理坐标后传给片元阶段:

attribute vec4 aFramePosition; attribute vec4 aTexCoords; uniform mat4 uTexTransform; varying vec2 vTexCoords; void main() { gl_Position = aFramePosition; vTexCoords = (uTexTransform * aTexCoords).xy; }

片元着色器

bitmap_overlay_video_processor_fragment.glsl 是叠加效果的灵魂。视频帧来自外部纹理(samplerExternalOES,需要声明GL_OES_EGL_image_external扩展),叠加位图是普通sampler2D

#extension GL_OES_EGL_image_external : require precision mediump float; // External texture containing video decoder output. uniform samplerExternalOES uTexSampler0; // Texture containing the overlap bitmap. uniform sampler2D uTexSampler1; // Horizontal scaling factor for the overlap bitmap. uniform float uScaleX; // Vertical scaling factory for the overlap bitmap. uniform float uScaleY; varying vec2 vTexCoords; void main() { vec4 videoColor = texture2D(uTexSampler0, vTexCoords); vec4 overlayColor = texture2D(uTexSampler1, vec2(vTexCoords.x * uScaleX, vTexCoords.y * uScaleY)); // Blend the video decoder output and the overlay bitmap. gl_FragColor = videoColor * (1.0 - overlayColor.a) + overlayColor * overlayColor.a; }

混合公式是标准的 alpha 合成:videoColor * (1 - overlayColor.a) + overlayColor * overlayColor.a。叠加位图透明区域 alpha 为 0,视频原样显示;图标与文字区域按 alpha 覆盖到视频之上。uScaleX/uScaleY让位图纹理坐标随视口尺寸拉伸,从而铺满屏幕宽度。

帧时间戳的对齐原理

demo 的核心亮点之一是"叠加内容包含当前帧的展示时间戳"。时间戳的获取依赖两条线索在 GL 线程上的精确对齐,实现在 VideoProcessingGLSurfaceView.java:

  1. VideoFrameMetadataListener.onVideoFrameAboutToBeRendered(presentationTimeUs, releaseTimeNs, format, mediaFormat)由播放器在每帧即将渲染时回调,把releaseTimeNs(帧应被释放/显示的系统时间)与presentationTimeUs(展示时间戳)配对存入TimedValueQueue<Long>
  2. onDrawFrame中,SurfaceTexture.getTimestamp()返回的是updateTexImage()时该帧的释放时间(纳秒),用它作为 key 去TimedValueQueue.poll()反查对应的展示时间戳,得到的就是当前正在绘制的这一帧的时间戳。
surfaceTexture.updateTexImage(); long lastFrameTimestampNs = surfaceTexture.getTimestamp(); @Nullable Long frameTimestampUs = sampleTimestampQueue.poll(lastFrameTimestampNs); if (frameTimestampUs != null) { this.frameTimestampUs = frameTimestampUs; } surfaceTexture.getTransformMatrix(transformMatrix);

TimedValueQueue(位于 library/common/src/main/java/com/google/android/exoplayer2/util/TimedValueQueue.java)维护了纳秒时间戳到展示时间戳的映射,poll会移除并返回时间不早于入参 key 的最近条目。这个机制是"让 GL 层知道屏幕上此刻显示的是哪一帧"的关键,也是叠加 OSD 信息(时间码、字幕、帧号)的通用做法。

受保护内容(DRM)与安全 GL 上下文

当通过 Intent 传入drm_scheme时,MainActivity 会先检查设备是否支持受保护内容扩展(GlUtil.isProtectedContentExtensionSupported),不支持则弹 Toast 提示。VideoProcessingGLSurfaceView的构造参数requireSecureContext控制是否创建安全 GL 上下文:

  • EGL 上下文属性追加EGL_PROTECTED_CONTENT_EXT = 0x32C0且值为EGL_TRUE(VideoProcessingGLSurfaceView.java);
  • 窗口 Surface 也追加同样的保护属性(VideoProcessingGLSurfaceView.java)。

同时视图选择 RGBA8888、无深度/模板缓冲的 EGL 配置(setEGLConfigChooser(8, 8, 8, 8, 0, 0)),并强制 GLES 2.0。DRM 会话则在initializePlayer中通过DefaultDrmSessionManager.Builder().setUuidAndExoMediaDrmProvider(...)HttpMediaDrmCallback构建,再以MediaSource.Factory.setDrmSessionManagerProvider注入。代码注释特别指出该视图必须用代码创建,因为是否创建安全上下文必须在构造时确定。

从 demo 到自定义渲染:可复用的改造路径

基于本 demo 的架构,接入自定义 GL 效果只需三步:

  1. 新建一个类实现VideoProcessingGLSurfaceView.VideoProcessor,在initialize()中加载你自己的 shader(可用 ExoPlayer 的GlProgram工具类简化 attribute/uniform 管理);
  2. draw()中用frameTexture采样视频帧(samplerExternalOES),叠加任意 GL 效果后输出;
  3. MainActivity中把BitmapOverlayVideoProcessor换成你的实现。

如需把画面渲染到 SurfaceView/TextureView 之外的目标(如编码器输入),仓库中 demos/transformer 与 demos/surface 提供了更多播放/渲染组合的参考。

迁移提示:从 com.google.android.exoplayer2 到 androidx.media3

该 demo 的全部源码位于com.google.android.exoplayer2包下,类上标注了@Deprecated,注释明确说明:com.google.android.exoplayer2已废弃,应迁移到 androidx.media3(其中包含相同的 ExoPlayer 代码),并提供了迁移脚本 media3-migration.sh。在 media3 中,本 demo 的VideoProcessor思想被 Transformer 的VideoProcessor接口(见 TransformerVideoRenderer 相关代码)进一步工程化,支持帧级处理与 GPU 管线;若在新项目中实现类似需求,建议直接基于 media3 编写。

小结

demos/gl虽是一个小型示例,却覆盖了 ExoPlayer 自定义渲染的完整技术栈:surface_type="none"的布局接管、GLSurfaceView+SurfaceTexture的离屏渲染、VideoFrameMetadataListener+TimedValueQueue的帧时间戳对齐、samplerExternalOES外部纹理采样、Canvas 位图叠加与 alpha 混合着色器,以及 DRM 场景的安全 EGL 上下文。它是理解 ExoPlayer 视频输出管线、进而实现滤镜/水印/OSD 等自定义渲染效果的绝佳起点。

【免费下载链接】ExoPlayerAn extensible media player for Android项目地址: https://gitcode.com/gh_mirrors/exop/ExoPlayer

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询