ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Android视频播放器实践:GSYVideoPlayer内核切换与缓存滤镜全解

Android视频播放器实践:GSYVideoPlayer内核切换与缓存滤镜全解 简介面向Android开发者的多功能视频播放器GSYVideoPlayer基于系统MediaPlayer并兼容ExoPlayer、IJKPlayer内核切换能够应对视频播放中的协议差异、缓存、弹幕、滤镜、画面旋转、列表播放等复杂需求适合中高级开发者学习或二次开发。压缩包共562个文件大小75.5MB以268个Java源码和127个XML布局/资源为主配合30个so动态库、27个Gradle构建脚本以及演示GIF、MP4等素材构成完整可运行的工程级项目。已有4699人学习/下载。功能上支持HTTPS、rtsp、hls、rtmp等常见协议边播边缓存提供20余种滤镜、水印、GIF截图、片头/中间广告、重力与手动旋转同步、列表播放全屏动画、进度条小窗预览、多分辨率切换等能力同时保留视频截图、镜像旋转、倍速播放等细节处理。从内核封装、缓存策略到界面交互均有清晰实现附带的说明文档和演示动图可帮助快速理解播放器架构与自定义扩展思路是研究Android视频播放方案的实用参考。1. 先搞清楚 GSYVideoPlayer 到底解决了什么问题做 Android 播放器的同学基本都经历过这种阶段先用 MediaPlayer 跑通本地视频接着发现 HLS 兼容性不行换 ExoPlayer后来接到需要支持 RTSP 的播放源又不得不再引入 IJKPlayer。更麻烦的是播放器只是一个起点后面的缓存、弹幕、滤镜、水印、GIF 截图、列表播放、重力旋转、多分辨率切换每一项需求都要自己对着 API 一个个磨。GSYVideoPlayer 的价值恰恰在于它把这些能力整合到了一套可切换的架构里同一个播放器界面底层可以在 IJKPlayer、ExoPlayer、MediaPlayer 之间切换上层通过 VideoPlayerController 统一管理。默认走 IJK 内核基于 FFmpeg通过 build.gradle 的 flavor 配置切到 ExoPlayer 或 MediaPlayer不需要改业务代码。这套设计尤其适合需要快速上线、又要兼容 Https / RTSP / concat / HLS 等协议的视频产品。本文基于该开源项目的源码结构展开讲清楚内核选型、缓存方案、滤镜管线、列表播放状态机以及常见坑的解法示例代码均可在 Android Studio 工程中直接编译验证。2. 播放内核选型与切换机制IJKPlayer、ExoPlayer、MediaPlayer 怎么共存2.1 为什么默认首选 IJKPlayer 而不是 ExoPlayerGSYVideoPlayer 默认内核是 IJKPlayer核心原因是 IJKPlayer 基于 FFmpeg协议覆盖面和音视频格式兼容性明显优于 ExoPlayer 和 MediaPlayer。比如 RTSP、concat、rtmp、mpeg 这些协议MediaPlayer 原生支持很差ExoPlayer 也得靠扩展库而 IJKPlayer 通过 FFmpeg 的 demuxer 基本都能解。另一个关键点是硬解码优先策略IJKPlayer 默认启用 MediaCodec软解只作为兜底。在项目里的具体表现是一旦你在 init 时调用GSYVideoManager.initPlayer(CorePlayerType.IJK)它内部会用GSYVideoType.setRenderType(GSYVideoType.SURFACE)去配合硬解渲染这比 MediaPlayer 的 TextureView 路径在帧延迟上更可控。提示如果你的播放场景集中在 HLS 和 DASH而且不要求 RTSP可以优先用 ExoPlayer它的自适应码率AdaptiveStream做得比 IJK 更稳。2.2 通过 Gradle flavor 配置内核切换GSYVideoPlayer 的源码工程用多渠道构建来区分内核而不是运行时动态换。它的 build.gradle 里配置了类似这样的结构productFlavors { noop {} exo {} ijk {} ijkExo {} }每个 flavor 引入不同的依赖dependencies { // ijk 内核 ijkImplementation com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0 ijkImplementation com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-arm64:v8.4.0 // exo 内核 exoImplementation com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-exo2:v8.4.0 }这里的关键点在依赖命名上gsyVideoPlayer-java只是纯 Java 层播放控制真正解码能力来自对应 abi 的 so 库gsyVideoPlayer-exo2会把 ExoPlayer 的依赖引入进来但它同时要求 VideoPlayer 层使用ExoPlayerManager。所以你在代码里不能写死new GSYVideoPlayer(this)之后直接调用setUp()而应该在 Application 初始化时指定内核GSYVideoManager.initPlayer(CorePlayerType.EXO);initPlayer(type)内部会根据 type 反射创建 PlayerManager。这里有个容易被忽略的细节如果你同时引入了 ijkExo 和 exo 两个 flavorGSYVideoManager会优先走 ExoPlayerManager所以线上包一般只保留一个 flavor避免 so 包体积过大。2.3 MediaPlayer 内核的适用边界MediaPlayer 在 GSYVideoPlayer 里被保留但不是主力。它的适用场景主要是本地文件播放和 Https 播放因为 MediaPlayer 依赖系统底层版本碎片化严重华为、小米、三星对 MediaPlayer 的硬件解码器表现差异较大。GSYVideoPlayer 的做法是把 MediaPlayer 和 IJKPlayer 的调用接口统一抽象成GSYVideoPlayerListener这样你在setUp()之后拿到getGSYVideoManager().getPlayer()实际返回的是MediaPlayerProxy或IJKPlayerProxy。如果你要自己接管底层的 TextureView 生命周期可以用setUp(String url, boolean cacheWithPlay, String cachePath, MapString, String mapHeadData)里的 mapHeadData 注入自定义 header。提示MediaPlayer 不支持 concat 协议但 IJK 支持。如果你用setUp()播放concat:file1|file2格式必须确保当前内核是 IJK。3. 缓存与协议层实现边播边缓存、Https 与 RTSP 的细节处理3.1 缓存机制的两条路径GSYVideoPlayer 的缓存设计不是自研一套而是直接复用 VideoCache 库。它的缓存逻辑核心是本地代理服务器播放器请求的是http://127.0.0.1:端口/视频urlVideoCache 内部启动一个轻量 HTTP Server把远程流拉下来写到本地文件同时响应给播放器。这样播放器本身感知不到网络流和缓存流的区别。在 GSYVideoPlayer 里启用缓存只需在setUp前设置player.setUp(url, true, null, null); // true 表示 cacheWithPlay即边播边缓存对应 ExoPlayer 的缓存策略则不同ExoPlayer 用的是SimpleCache它直接操作 CacheDataSourceSimpleCache simpleCache new SimpleCache( new File(getExternalCacheDir(), video_cache), new LeastRecentlyUsedCacheEvictor(1024 * 1024 * 200), // 最大缓存 200MB new ExoDatabaseProvider(this) ); CacheDataSource.Factory cacheDataSourceFactory new CacheDataSource.Factory() .setCache(simpleCache) .setUpstreamDataSourceFactory(new DefaultDataSource.Factory(this));这里注意 LeastRecentlyUsedCacheEvictor 的淘汰策略是 LRU它只在你设定的最大容量范围内保留缓存。如果你的业务需要控制单个视频的缓存时间可以用Evictor自定义策略而不是直接在 SimpleCache 上设置过期时间。3.2 Https 播放的证书处理Https 在 IJKPlayer 里默认是能播的但问题往往出在自签名证书或双向认证上。如果你遇到SSL handshake failed先确认是否走的是 IJK 内核然后在初始化时设置IJKPlayerManager.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, dns_cache_timeout, 30); IJKPlayerManager.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, reconnect, 1); IJKPlayerManager.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, http-detect-range-support, 0);http-detect-range-support0是告诉 FFmpeg 不要探测服务器是否支持 Range 请求这在很多 CDN 场景下能避免播放器因为服务器没回 206 导致从头拉流。另外对于自签名证书的 Https 视频源可以设置IJKPlayerManager.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, verify, 0);但 verify0 是全局的不建议在生产环境长期开。常见的做法是通过 OkHttp 拦截请求自定义证书校验再把 OkHttp 的 Call 传给播放器的mapHeadData。3.3 RTSP 拉流的参数调优RTSP 是 GSYVideoPlayer 的强项场景之一。它的底层走 IJK 的 RTSP demuxer默认情况下 FFmpeg 会用 TCP 模式传输但公网丢包严重时RTSP over UDP 反而能撑住。在项目里我一般这样配置IJKPlayerManager.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, rtsp_transport, tcp); IJKPlayerManager.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, rtsp_flags, prefer_tcp); IJKPlayerManager.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, stimeout, 3000000);stimeout的单位是微秒3000000 表示 3 秒超过这个时间 RTSP 连接没有响应就直接失败避免播放器一直转圈。这里有个容易踩的坑如果视频源是 RTSP H.265IJK 默认的硬解 MediaCodec 在部分机型上不支持 H.265你会看到有声音没画面或者直接黑屏。解决办法是强制软解IJKPlayerManager.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec, 0); IJKPlayerManager.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec-auto-rotate, 0);强制软解后 CPU 占用会升高但至少能出画面。手机发热问题得靠后续的软解渲染优化去压。提示RTSP 流的stimeout参数是 IJK 内核特有的ExoPlayer 不支持该选项。如果你的场景是 RTSP 为主建议单独做一套 exo 禁用配置。4. 画面渲染与滤镜管线水印、GIF 截图、旋转与多分辨率4.1 渲染层结构SurfaceView 与 TextureView 的取舍GSYVideoPlayer 的渲染层默认使用 GSYSurfaceView支持直接在 Surface 上绘制滤镜。它对视频帧的处理路径是GSYSurfaceView - GSYVideoGLView - GLSurfaceView。为什么不用 TextureViewTextureView 虽然能直接做矩阵变换但在部分低端机上画面撕裂明显而且无法直接绑定 GL 管线。GSYVideoPlayer 的滤镜恰恰是跑在 GL 层所以它用 TextureView 做前置缓冲、GSYVideoGLView 做最终输出的方式绕开了系统级 SurfaceView 的兼容坑。如果你不需要滤镜只想降低延迟可以在init()里改成GSYVideoType.SURFACEGSYVideoType.setRenderType(GSYVideoType.SURFACE);4.2 简单滤镜与自定义滤镜项目内置 20 多种简单滤镜调用入口统一在GSYVideoGLView上GSYVideoGLView.ShaderInterface effectFilter new GSYVideoGLView.SimpleFilter( GSYVideoGLView.SimpleFilter.FILTER_BLACK_WHITE ); player.getGSYVideoGLView().setEffectFilter(effectFilter);支持的滤镜类型包括马赛克、黑白、色彩过滤、高斯模糊、暖色、冷色等。自定义滤镜需要继承 ShaderInterface重写getShader()public class CustomFilter implements GSYVideoGLView.ShaderInterface { private final String mShader #extension GL_OES_EGL_image_external : require\n precision mediump float;\n varying vec2 vTextureCoord;\n uniform samplerExternalOES sTexture;\n void main() {\n vec4 color texture2D(sTexture, vTextureCoord);\n float grayscale dot(color.rgb, vec3(0.299, 0.587, 0.114));\n gl_FragColor vec4(grayscale, grayscale, grayscale, 1.0);\n }\n; Override public String getShader() { return mShader; } }这里最核心的点是uniform samplerExternalES它绑定的是外部纹理而不是标准 2D 纹理。写自定义滤镜时必须保留这一行声明否则 GL 编译直接报错。另外滤镜只对视频画面生效不影响字幕和弹幕层因为弹幕和字幕是走独立的 View 层级。4.3 GIF 截图与视频帧截图GIF 截图功能在项目里对应GSYVideoGifView它通过循环抽取视频帧合成 GIF。核心调用方式是GSYVideoGifView gifView new GSYVideoGifView(this); gifView.startGif( new File(getCacheDir(), output.gif), player, 0.5f, 400 );参数说明0.5f表示抽取间隔单位是秒也就是每 0.5 秒取一帧400是宽高参数这里代表宽度为 400高度按原视频比例自动缩放。GIF 合成的性能瓶颈在解码和编码的串行处理。项目内部用 Bitmap 序列合成如果视频分辨率太高GIF 会比播放慢很多。我一般会把源视频先做一次尺寸缩放再交给 GIF 生成器避免在 1080p 视频上直接生成大尺寸 GIF 导致 OOM。视频帧截图相对简单GSYVideoPlayer 提供了Bitmap bitmap player.getCurrentFrame(); // 获取当前帧返回 Bitmap这个 API 在暂停状态下最稳定如果正在播放中调用需要先player.setPlayerState(GSYVideoPlayer.CURRENT_STATE_PAUSE)否则部分机型上拿到的帧是黑屏。4.4 视频自身 rotation 属性与重力旋转的同步逻辑很多视频文件本身带有 rotation 信息比如手机竖屏拍出的视频rotation 是 90 或 270。GSYVideoPlayer 的处理逻辑是IJK 内核拿到 rotation 后会自动旋转画面但自动旋转只生效一次如果你手动旋转了界面后续的视频 rotation 不会再次触发。这里有一个内置接口player.setRotateWithSystem(true); // 跟随系统重力感应如果你要读取视频原生的 rotation 角度可以通过IJKMediaMeta meta player.getMediaMeta(); int rotation meta.getInt(IJKMediaMeta.IJK_KEY_ROTATION);拿到 rotation 后根据 90/270 去调整列表页的封面图方向比直接设置播放器旋转要省资源。4.5 多分辨率切换的实现思路分辨率切换不是播放器内部功能而是基于同一个 VideoUrl 的不同清晰度版本。GSYVideoPlayer 的做法是切换 URL 并重新setUp()同时保留当前播放进度int currentPosition player.getCurrentPositionWhenPlaying(); player.setUp(newUrl, true, null, null); player.seekTo(currentPosition);需要注意的是切换分辨率前先暂停播放等onPrepared回调后再seekTo否则在部分机型上会跳到片头。更好的方案是预加载一条新 URLready 后瞬间切换这是直播类的常见做法。5. 列表播放与多播放器并发小窗口拖动、全屏动画、无缝衔接的坑5.1 多播放器并发的实现边界GSYVideoPlayer 的多同时播放指的是GSYVideoPlayer多个实例可以同时运行但不是无限制的。它基于GSYVideoManager的单例设计一个 Manager 维护一个播放核心。如果同时初始化多个播放器实例核心解码器依然只有一个其他实例要么等待要么直接失败。官方方案是GSYVideoManager.instance().setListener()做多播放器切换而不是真的并发解码。实际项目中要同时播放两个视频可以创建两个GSYVideoManager实例但这样 CPU 和內存占用会翻倍。我在智慧大屏项目里测试过同时两路 720p 视频流内存占用约 400MB低端机上直接卡顿。因此建议用两个实例前先把页面首帧和封面图做占位播放器进入前台时才真正初始化。5.2 列表播放状态机与回收列表播放是 GSYVideoPlayer 最常用的场景之一。核心思路是 RecyclerView 滑动时只允许一个播放器处于播放态其他条目复用 ItemView 显示封面。项目的GSYVideoPlayer有 8 个状态状态含义触发时机CURRENT_STATE_IDLE空闲初始化/释放后CURRENT_STATE_PREPAREING正在准备setUp()后CURRENT_STATE_PLAYING播放中onPrepared后CURRENT_STATE_PAUSE暂停手动暂停/音频焦点变化CURRENT_STATE_ERROR播放失败网络异常/解码失败CURRENT_STATE_AUTO_COMPLETE自动播完播放至结尾CURRENT_STATE_PLAYING_BUFFERING缓冲中网络卡顿CURRENT_STATE_PREPAREING_ABORT准备中断用户在 prepare 期间滑走在列表滑动时你应该在onScrollStateChanged里停止当前播放器recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { Override public void onScrollStateChanged(NonNull RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState RecyclerView.SCROLL_STATE_DRAGGING) { GSYVideoManager.instance().pause(); } else if (newState RecyclerView.SCROLL_STATE_IDLE) { GSYVideoManager.instance().start(); } } });注意pause()和start()是管理器级别的操作它会作用于当前正在播放的视频。如果你在详情页和列表页之间跳转页面销毁时必须调用GSYVideoManager.releaseAllVideos()释放否则解码器会残留导致下一次播放黑屏。5.3 小窗口拖动与列表全屏动画小窗口播放是列表需求中常见的交互。GSYVideoPlayer 内置了GSYVideoSmallPlayer直接调用player.startWindowFullscreen(context, false, true);在窗口模式下播放器会被移到一个独立的 WindowManager 容器里通过自定义触摸监听实现拖动smallPlayer.setOnTouchListener((v, event) - { if (event.getAction() MotionEvent.ACTION_MOVE) { WindowManager.LayoutParams params smallPlayer.getWindowLayoutParams(); params.x (int) event.getRawX() - smallPlayer.getWidth() / 2; params.y (int) event.getRawY() - smallPlayer.getHeight() / 2; smallPlayer.updateWindowLayout(params); } return true; });列表全屏动画的关键点在setFullViewContainer和setToggleFullScreen的时机。全屏切换应该延迟到onClick事件后 100ms 再执行避免 RecyclerView 的 item 复用导致 View 已经被回收到不可见位置。提示小窗口模式下如果列表所在的 Activity 设置了configChanges请确认 Manifest 里android:configChangesorientation|screenSize已配置否则旋转屏幕时小窗口会直接崩溃。5.4 列表切换详情页无缝播放这个需求本质是 View 层的状态转移。GSYVideoPlayer 的做法是把当前播放器实例转移到详情页的容器中项目里对应的 API 是player.setPlayTag(detail); GSYVideoManager.instance().setListener(new GSYVideoPlayerListener() { Override public void onStartPrepared(String url, Object... obj) { // 传递播放进度到详情页 detailPlayer.seekTo(player.getCurrentPositionWhenPlaying()); } });无缝播放的难点不在 View 转移而在 Uri 和缓存的衔接。如果详情页重新setUp()同一个 URL缓存机制会命中本地文件所以基本能做到秒开但如果你切换了清晰度 URL缓存不命中就会重新拉起网络流无缝就失效了。合理的做法是详情页直接复用列表页的 PlayUrl不重新构造地址。6. 进阶验证技巧自定义内核后如何确认实际播放路径拿到源码后建议做的第一件事不是改功能而是加日志验证实际的播放内核和协议加载路径。具体做法是在GSYVideoManager的initPlayer里打点Log.i(GSY-NEW, initPlayer: type type , playerClass player.getClass().getName());通过这一段日志你能立刻确认当前项目编译的是 IJK、Exo 还是 MediaPlayer 内核。进一步检查 FFmpeg 实际解码器是否启用可以在IJKPlayerManager中打开 verbose 日志IJKPlayerManager.setLogLevel(IjkMediaPlayer.IJK_LOG_DEBUG); player.setOnInfoListener((what, extra) - { if (what IjkMediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) { Log.i(GSY-NEW, video rendering start, decoder player.getVideoDecoder()); } return false; });getVideoDecoder()返回的是具体解码器名称比如MediaCodec(OMX.qcom.video.decoder.avc)或ffmpeg。判断硬解是否生效就看这个返回值是否带MediaCodec前缀。软解的话返回值通常是ffmpeg。另一个实用技巧是抓取播放器底层对网络请求的处理。在PlayerManager的子类中重写startPlay()后把getDataSource()打出来能确认缓存代理是否生效。如果 URL 前缀是http://127.0.0.1:xxxxx说明 VideoCache 在正常代理如果看到的是原始地址说明cacheWithPlay没生效通常是 url 带 query 参数导致代理库跳过了缓存逻辑。提示验证 RTSP 是否走 TCP 模式可以抓包看端口 554 的连接状态也可在启动播放前用adb shell setprop log.tag.VLC 3观察底层网络连接日志。最后一个技巧是关于 ExoPlayer 内核下自定义 DASH 轨道的。GSYVideoPlayer 的 Exo 内核支持ExoSourceManager自定义 DataSource你在接入 DRM 或者自研播放器时可以直接替换ExoPlayerManager中的DefaultDataSource.Factory这样就不需要改动上层 VideoPlayer 的setUp()逻辑。这个点很容易被忽略很多人会在onPrepared里手动构造自定义 ExoPlayer结果绕开了 GSYVideoPlayer 的缓存和状态机导致后续所有 UI 逻辑失效。正确的做法是继承ExoPlayerManager并重写createExoPlayer()在里面把 CustomDataSource 注入进去保持上层状态机不动。本文还有配套的精品资源点击获取
返回列表