尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

ExoPlayer GL demo 深度解析:用 GLSurfaceView + GL Shader 实现视频位图叠加渲染 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 StudioFile → 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支持http、https、content、asset、file五种 scheme因此也可以通过隐式 Intent 播放任意视频地址例如adb shell am start -a com.google.android.exoplayer.gldemo.action.VIEW -d https://example.com/video.mp4Intent 还支持三个可选 extraextension强制指定媒体类型扩展名、drm_scheme如cenc、drm_license_urlDRM License 服务地址详见下文。渲染链路总览ExoPlayer 如何把帧交给 GL整个 demo 的架构核心是 VideoProcessingGLSurfaceView.java。它与普通GLSurfaceView用法的区别在于视频解码输出不再直接交给系统 Surface而是交给一个离屏SurfaceTextureGL 渲染线程再把它当作外部纹理samplerExternalOES采样、加工并绘制到窗口。布局双视图叠加main_activity.xml 中StyledPlayerView设置了app:surface_typenone即播放器不创建自己的 Surface随后 MainActivity 把VideoProcessingGLSurfaceView以代码方式添加到R.id.exo_content_frameStyledPlayerView预留的内容容器中形成控制 UI 在播放器视图层、视频画面在 GL 视图层的叠加结构。播放器接线MainActivity.initializePlayer() 做了三件关键的事根据 Intent 与Util.inferContentType判断内容类型只支持 DASHDashMediaSource与普通渐进式文件ProgressiveMediaSource两类构建ExoPlayer设置循环播放REPEAT_MODE_ALL将同一个 player 同时挂到StyledPlayerView与VideoProcessingGLSurfaceViewvideoProcessingGLSurfaceView.setPlayer(player); playerView.setPlayer(player); player.addAnalyticsListener(new EventLogger());其中setPlayer()内部VideoProcessingGLSurfaceView.java分别调用了player.setVideoSurface(surface)把解码输出导向SurfaceTexture对应的 Surface以及player.setVideoFrameMetadataListener(renderer)让渲染器在每帧即将渲染时拿到时间戳与格式信息。帧到达驱动渲染VideoRendererGLSurfaceView 的内部 Renderer在onSurfaceCreated时创建外部纹理与SurfaceTexture并注册OnFrameAvailableListener——每收到一帧就把frameAvailable原子标志置位并调用requestRender()VideoProcessingGLSurfaceView.java。视图使用setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY)即仅当新帧到达时才触发渲染避免空转耗电。在onDrawFrame中VideoProcessingGLSurfaceView.java完成核心四步首次绘制时调用videoProcessor.initialize()初始化 shader 程序与纹理有尺寸变化时调用videoProcessor.setSurfaceSize(width, height)有新帧时调用surfaceTexture.updateTexImage()更新外部纹理并通过getTimestamp()获取 SurfaceTexture 侧的时间戳再用sampleTimestampQueue.poll(lastFrameTimestampNs)反查出该帧的展示时间戳最后getTransformMatrix取纹理坐标变换矩阵调用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构造器视图本身完全不需要改动。BitmapOverlayVideoProcessorCanvas 位图叠加的完整实现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/Y在setSurfaceSize中按视口尺寸与位图尺寸之比计算用于片元着色器中把位图纹理坐标缩放适配到屏幕。着色器详解顶点着色器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.javaVideoFrameMetadataListener.onVideoFrameAboutToBeRendered(presentationTimeUs, releaseTimeNs, format, mediaFormat)由播放器在每帧即将渲染时回调把releaseTimeNs帧应被释放/显示的系统时间与presentationTimeUs展示时间戳配对存入TimedValueQueueLong在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_TRUEVideoProcessingGLSurfaceView.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 效果只需三步新建一个类实现VideoProcessingGLSurfaceView.VideoProcessor在initialize()中加载你自己的 shader可用 ExoPlayer 的GlProgram工具类简化 attribute/uniform 管理在draw()中用frameTexture采样视频帧samplerExternalOES叠加任意 GL 效果后输出在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_typenone的布局接管、GLSurfaceViewSurfaceTexture的离屏渲染、VideoFrameMetadataListenerTimedValueQueue的帧时间戳对齐、samplerExternalOES外部纹理采样、Canvas 位图叠加与 alpha 混合着色器以及 DRM 场景的安全 EGL 上下文。它是理解 ExoPlayer 视频输出管线、进而实现滤镜/水印/OSD 等自定义渲染效果的绝佳起点。【免费下载链接】ExoPlayerAn extensible media player for Android项目地址: https://gitcode.com/gh_mirrors/exop/ExoPlayer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表