news 2026/9/16 9:49:38

Android视频播放器内核选型:GSYVideoPlayer的IJKplayer与ExoPlayer实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android视频播放器内核选型:GSYVideoPlayer的IJKplayer与ExoPlayer实战

简介:一份面向安卓开发者的多功能视频播放器完整源码工程,集成ijk、系统与exo三种可切换的播放内核,支持多种网络协议,实现了边播边缓存、数十种滤镜、水印、GIF截图、弹幕、外挂字幕、列表播放、重力旋转与手动旋转同步、多分辨率切换等丰富特性,适合安卓音视频工程师、播放器组件二次开发人员以及开源框架研读者参考。压缩包共五百六十二个文件,容量七十五点五兆,主体包括两百六十八个Java源文件、一百二十七个XML布局与配置文件、三十个SO动态解码库,并附有Gradle构建脚本、工程说明文档、演示用图片动图及示例音视频,便于在集成开发环境中直接导入与分析。已有四千六百九十九人学习,资源质量及功能性经过较多用户验证。借助该工程可梳理播放器内核适配层、边播边缓存策略、滤镜渲染管线与列表无缝切换等关键模块的代码实现,同时可参考其弹幕、广告、小窗、进度条预览等功能的组织方式,为后续商业项目扩展或源码级二次开发提供扎实基础。

1. GSYVideoPlayer 到底替你干了哪些活

做过 Android 播放器的同学都清楚,系统自带 MediaPlayer 只适合 demo 级别的播放需求,一旦涉及 HTTPS、rtsp、边播边缓存、列表复用、全屏动画这些词,就要自己造轮子。GSYVideoPlayer 在 GitHub 上被大量项目引用,是因为它把 IJKplayer、ExoPlayer、MediaPlayer 三套播放内核统一成一套对外 API,再从这套 API 往上补了弹幕、外挂字幕、滤镜、水印、GIF 截图、广告插播、多实例播放、重力旋转同步、进度条帧预览等一整套播放器周边能力。与其说它是一个播放器,不如说它是一个「已经帮你踩过一遍坑的播放器框架」。这篇文章会从内核选型开始,把网络层、渲染层、列表交互和 rtsp 接入这几个最容易出问题的地方逐个拆开讲。

2. IJKplayer、ExoPlayer、MediaPlayer 三内核怎么选、怎么切

2.1 IJKplayer:协议最全,但维护成本摆在那

IJKplayer 基于 FFmpeg,B 站开源之后一直是国内直播、点播 App 的首选内核,支持 rtsp、rtmp、mms 这类 ExoPlayer 很难搞定的协议。社区里常听到 ijkplayer 6.1.1 这个版本号,它对应的 FFmpeg 内核已经足够新,硬解走 MediaCodec,软解走 FFmpeg,遇到系统解码器不支持的编码格式时能兜底。缺点是 so 体积大,而且原作者已经很久不更新,很多机型的新问题要自己改源码编译。

如果你要在 WSL 下编译 ijkplayer,常见做法是装好 NDK r21e,拉源码后直接跑compile-ijkplayer.sh。真正费时间的是 FFmpeg 的 configure 步骤,尤其是想开 https、rtmp 或者 rtsp over tcp 时,需要在 configure 里手动指定 openssl 和对应的 protocol 开关,默认的--disable-protocol=*会把一大堆协议关掉。编出来的 so 有多个 ABI,实际打包时只保留 arm64-v8a 和 armeabi-v7a 就能省下不少体积。

2.2 ExoPlayer:自适应流和 HTTPS 的主场

ExoPlayer 是 Google 官方维护的播放内核,在 GSYVideoPlayer 里承担的是「现代播放器」角色。它原生支持 HLS、DASH、SmoothStreaming 这类自适应码率协议,带宽变化时自动切分辨率,HTTP 层的扩展性也更好,可以用 OkHttp 替换底层网络栈,处理自签证书、拦截日志、自定义 Header 都比 IJKplayer 顺手。GSYVideoPlayer 的缓存代理层和 ExoPlayer 配合也最顺,因为 ExoPlayer 通过 DataSource 接口访问网络,代理层可以透明插入。

rtsp 在 ExoPlayer 里属于后来才加的协议,支持程度明显不如 IJKplayer;rtmp 基本不用考虑。所以选型口诀是:点播、自适应流、HTTPS 场景优先 ExoPlayer,rtsp、rtmp 直播源优先 IJKplayer。

2.3 MediaPlayer:轻量场景的兜底

系统 MediaPlayer 封装的是系统解码器和底层播放框架,优点是零依赖、不引入 so 文件,适合音频播放、TV 盒子、车机这类对视频增强功能没有要求的场景。缺点是扩展能力几乎为零,不支持边播边缓存代理,在列表复用场景下也没有现成的全屏动画机制,而且不同厂商 ROM 对 MediaPlayer 的底层实现差异很大。GSYVideoPlayer 保留这个内核,主要目的是让项目在不需要复杂视频功能时减少包体。

2.4 用 PlayerFactory 切换 GSYVideoPlayer 的播放内核

GSYVideoPlayer 在不同版本里的切换方式略有差异,常见做法是基于PlayerFactory设置全局播放内核:

// 切到 IJKplayer 内核,适合 rtsp、rtmp、直播源 PlayerFactory.setPlayManager(IjkPlayerManager.class); // 切到 ExoPlayer 内核,适合 HTTPS、HLS、自适应码率 PlayerFactory.setPlayManager(ExoPlayerManager.class); // 切到系统 MediaPlayer PlayerFactory.setPlayManager(SystemPlayerManager.class);

这段代码必须在创建播放器之前调用。IjkPlayerManager内部持有IjkMediaPlayer,底层走 FFmpeg 解码链路;ExoPlayerManager包的是SimpleExoPlayer,对外暴露的接口与 IJK 保持一致;SystemPlayerManager则把系统MediaPlayer封装成同一套接口。业务代码不直接操作内核,只面向GSYVideoPlayer暴露的方法,所以内核切换对上层透明。

硬解与纹理渲染的开关也需要在初始化阶段配合设置:

// 优先硬解 GSYVideoType.enableMediaCodec(); // 硬解 + 纹理渲染,滤镜、水印、GIF 截图依赖这个模式 GSYVideoType.enableMediaCodecTexture(); // 回到软解 GSYVideoType.disableMediaCodec();

enableMediaCodecTexture()是后面讲滤镜和截图的关键前置条件,它让视频帧先进入 OpenGL 纹理,再由GSYVideoPlayer去做二次处理。enableMediaCodec()只硬解不做纹理处理,适合纯播放场景。要注意同一个播放器实例内切换内核没有意义,内核切换只对之后新建的播放器实例生效。

内核rtsp/rtmpHTTPSHLS/DASH边播边缓存滤镜/水印维护成本
IJKplayer需编译 openssl一般支持支持
ExoPlayer支持支持
MediaPlayer不支持一般部分不支持不支持

实际项目里最常见的做法是默认 IJKplayer,视频源是 HLS 或需要 OkHttp 注入 Header 时动态切 ExoPlayer。切换后要重新setUp播放地址,因为底层播放器实例已经重建,播放进度需要自己保存并在 prepare 完成后 seek 回去。

3. HTTPS、边播边缓存与进度条预览:网络数据层怎么调

3.1 HTTPS:证书校验、明文限制与 OkHttp 拦截

GSYVideoPlayer 本身不决定 HTTPS 通不通,真正起作用的是内核的网络栈。IJKplayer 走 FFmpeg 的 TLS 实现,需要编译时加入 openssl;ExoPlayer 默认走DefaultHttpDataSource,可以替换成 OkHttp 通道。Android 9 之后系统默认禁止明文流量,如果你的视频源是http://地址,需要先处理网络安全配置:

<!-- res/xml/network_security_config.xml --> <network-security-config> <base-config cleartextTrafficPermitted="false" /> <domain-config cleartextTrafficPermitted="true"> <domain includeSubdomains="true">your-video-server.com</domain> </domain-config> </network-security-config>

然后在 Manifest 的 application 节点引用它:

<application android:networkSecurityConfig="@xml/network_security_config" ... />

cleartextTrafficPermitted只对域名生效,避免为了一个测试地址把整个应用的明文开关全部打开。release 包如果要支持自签证书,正确做法是把证书打入应用或做证书 pinning,不是在全局信任所有证书。

调试时如果想抓 HTTPS 明文,ExoPlayer 配合 OkHttp 拦截器是最省事的路径:

OkHttpClient client = new OkHttpClient.Builder() .sslSocketFactory(sslContext.getSocketFactory(), trustManager) .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) .build(); OkHttpDataSource.Factory factory = new OkHttpDataSource.Factory(client); // 把 factory 设置到 ExoPlayerManager 对应的数据源工厂 exoPlayerManager.setDataSourceFactory(factory);

HttpLoggingInterceptor会把请求头和响应体打到日志里,排查 403、鉴权失败、防盗链签名过期这类问题非常快。注意这只适合 debug 环境,release 环境要移除trustManager的信任所有证书逻辑,否则等于关闭了 HTTPS 的证书校验。

3.2 边播边缓存:缓存目录、上限与直播流的例外

GSYVideoPlayer 的边播边缓存核心是 AndroidVideoCache 这套代理方案,它在本地开一个 HTTP 服务,把远程视频地址包装成http://127.0.0.1:port/真实地址,播放器请求本地代理,代理边下载边保存文件。常见初始化方式:

// Application 中初始化缓存服务 HttpProxyCacheServer cacheServer = new HttpProxyCacheServer.Builder(this) .maxCacheSize(1024 * 1024 * 1024L) // 缓存上限 1GB .cacheDirectory(new File(getCacheDir(), "video_cache")) .build(); GSYVideoManager.setProxy(cacheServer);

maxCacheSize决定缓存文件达到多大后开始淘汰旧文件;cacheDirectory建议放在应用私有目录,避免用户清理 App 数据后残留大量视频文件。缓存文件名的生成基于 URL 的 Hash,同一 URL 二次播放时直接走本地文件,没有网络请求。

缓存协议只对支持 Range 的 HTTP 源有效。rtsp 直播源没有 Content-Length,代理无法预知文件大小,硬走缓存会把直播流当无限长文件一直写盘,最终磁盘写满,播放延迟也越来越大。所以直播场景必须关闭缓存:

videoPlayer.setUp(url, false, null); // 第二个参数 live 传 false 表示不是直播 videoPlayer.setCacheWithPlay(false); // 明确关闭边播边缓存

判断一个地址能不能缓存,最简单的标准是看服务器响应有没有Accept-RangesContent-Length头。rtmp、rtsp、大部分 m3u8 切片流都不适合直接套缓存代理。

3.3 视频加载速度:分析时长、probesize 与首屏优化

视频起播慢的常见原因是播放器在解析媒体信息阶段花太多时间。FFmpeg 内核默认会分析一段数据才返回 prepare 完成,rtsp 直播源尤其明显:

IjkMediaPlayer ijkPlayer = (IjkMediaPlayer) player; // 分析时长,单位是微秒,rtsp 直播可以压到 500ms 以内 ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", "500000"); // 探测缓冲区大小,单位字节,默认值较大时会拖慢起播 ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", "1024"); // 关闭额外缓冲,边下边播 ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");

analyzeduration调小后,遇到头部信息复杂的视频文件可能解析失败,所以点播源建议设置在 1 秒到 2 秒。probesize对应分析数据的大小,调小可以加快起播,但过小会导致部分视频的 meta 信息读取不完整。这两组参数不是越大越好,要按视频源类型分别设置。

ExoPlayer 内核则通过LoadControl控制缓冲水位,默认配置偏保守。如果你要追求首屏速度,可以自定义DefaultLoadControl,把bufferForPlaybackMs调到 1500ms 左右,减少首帧前等待的数据量。

3.4 进度条小窗口预览:从帧抽取到暂停预览

进度条小窗口预览有两种实现路线。一种是用MediaMetadataRetriever.getFrameAtTime()抽缩略图,适合列表页 hover 显示,缺点是每次拖动都要解码关键帧,频繁拖动时 CPU 占用高、缩略图延迟明显。另一种是 GSYVideoPlayer 更常用的「暂停预览」方案:手指拖动 SeekBar 时,播放器 seek 到目标位置并暂停,画面停在对应帧;松手后恢复播放:

private long lastPreviewTime; private void previewTo(long targetPosition) { // 防抖:拖动回调频率远高于 seek 能处理的速度 if (System.currentTimeMillis() - lastPreviewTime < 300) { return; } lastPreviewTime = System.currentTimeMillis(); player.seekTo(targetPosition); player.setSpeed(0f); // 部分内核支持 0 速暂停预览 } @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (!fromUser || player == null) return; long position = (long) (progress * 1f / 1000 * player.getDuration()); previewTo(position); } @Override public void onStopTrackingTouch(SeekBar seekBar) { player.setSpeed(1f); player.seekTo(currentSeekPosition); player.start(); }

setSpeed(0f)不是所有内核都支持,MediaPlayer 内核需要改成player.pause(),IJKplayer 和 ExoPlayer 则可以直接用 0 速。预览画面的小窗口本质上还是同一个播放器的渲染画面,做法是在拖动时把播放器容器从大屏区域移到小窗 FrameLayout 中,或者让 TextureView 的 LayoutParams 从小窗口尺寸变为全屏尺寸。注意 seek 到关键帧间隔较大的视频时,实际显示的画面不一定精确落在目标秒数,这是 GOP 编码结构决定的,不是 bug。

提示:进度条预览频繁触发onProgressChanged时,CPU 和内存开销都会上升,防抖阈值建议保持在 200ms 到 300ms。如果预览窗口还要显示时间气泡,时间文本的更新频率与防抖保持一致,避免 UI 线程被刷爆。

4. 旋转同步、列表播放、全屏动画与小窗口拖动

4.1 视频自带 rotation 与重力旋转的同步规则

视频文件编码时,采集设备的传感器方向会以 rotation 字段写进轨道信息,常见值是 0、90、180、270。播放器读取这个字段后,在渲染阶段把画面旋转到正确方向。GSYVideoPlayer 里对应的是setRotateViewAuto(true),它让播放器自动处理视频自带的方向。

问题出在同时开启重力感应时。如果设备横屏且视频自带 rotation 90,两个旋转叠加,画面会变成颠倒的。处理规则是:rotation 为 0 和 180 的视频可以跟随系统重力旋转,rotation 为 90 和 270 的视频以视频自带旋转为准,不再叠加系统方向:

videoPlayer.setRotateViewAuto(true); videoPlayer.setRotateWithSystem(true);
视频 rotation是否启用重力旋转最终显示效果
0跟随设备横竖屏
180跟随设备横竖屏
90画面自动立起
270画面自动立起

如果你发现视频横屏播放时比正常方向多转了一圈,多半是setRotateViewAutosetRotateWithSystem同时生效导致的重复旋转。排查时先关掉重力旋转,只保留setRotateViewAuto,确认画面方向正确后再打开系统旋转;反之亦然。

4.2 列表播放:item 复用、全屏动画与无缝跳详情

RecyclerView 里放多个播放器是高频翻车场景。item 滑出屏幕后,播放器 View 被回收,但播放器内核实例可能还在运行,导致滑回来时画面错乱或者声音还在播。常见做法是让列表只维护一个当前播放实例,item 被回收时统一释放:

@Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { super.onViewRecycled(holder); // 判断回收的是不是当前正在播放的 item if (holder.getAdapterPosition() == currentPlayPosition) { GSYVideoManager.releaseAllVideos(); } }

GSYVideoManager.releaseAllVideos()会把内部持有的播放器实例、Surface 和网络资源全部释放。配合GSYVideoManager.setListener监听onAutoCompletion,在当前视频播完后自动切换下一个 item 的播放。注意释放时不要直接把 item 里的 View 设置为 gone,否则 item 复用时会闪黑屏。

列表全屏动画是 GSYVideoPlayer 的强项之一,它把 item 中的小窗口播放器通过坐标变换平滑放大到全屏。相关开关包括:

videoPlayer.setShowFullAnimation(true); // 开启进入全屏的缩放动画 videoPlayer.setLockLand(true); // 强制全屏后横向 videoPlayer.setFullHideAction(GSYVideoPlayer.FULLSCREEN_ENTER_EXIT);

setFullHideAction只在全屏动画中使用,它指定全屏页隐藏状态栏和导航栏的时机。动画卡顿通常是因为 item 位置信息获取失败,播放器会退化为直接全屏,没有中间动画。确认列表 item 的父容器不要提前执行setVisibility(GONE),否则无法计算起始坐标。

列表切换详情页无缝播放的核心是保持播放器实例不销毁。点击 item 时把播放器从列表容器中摘出来,交给详情页容器,Activity 跳转完成后播放器继续渲染画面:

ViewGroup parent = (ViewGroup) player.getParent(); if (parent != null) { parent.removeView(player); } detailContainer.addView(player);

这里要求播放器必须走 GSYVideoManager 的单实例模式,GSYVideoManager.releaseAllVideos()在跳转瞬间不能调用,否则播放器会直接销毁。切页时暂停播放还是继续播放,取决于产品需求,但暂停时不要调用release,只需要player.onPause()player.onResume()

4.3 小窗口拖动、声音亮度调节与调整比例

列表小窗口拖动是 GSYVideoPlayer 对外提供的一套浮动窗口能力。开启小窗口拖动后,播放器 View 可以脱离列表容器,在屏幕内移动,收到触摸事件时自己处理坐标变化:

videoPlayer.setPlayTag("float"); videoPlayer.setSmallVideoTextureView(new TextureView(context));

拖动逻辑要区分「点击」和「拖动」,不能一收到ACTION_MOVE就移动播放器。常见做法是用 ViewConfiguration 的getScaledTouchSlop()判断位移阈值,小于阈值视为点击,继续走原本的点按逻辑;超过阈值才进入拖动状态,并修改播放器所在容器的 LayoutParams 坐标。

声音和亮度调节在 GSYVideoPlayer 里默认是手势控制,播放器左侧上下滑动调亮度,右侧上下滑动调音量,横向滑动调进度。这套手势开关是:

videoPlayer.setIsTouchWiget(true); // 开启触摸手势 videoPlayer.setIsTouchWigetFull(false); // 是否仅全屏时响应手势

setIsTouchWiget的小写写法是历史遗留命名,很多老项目里就是这么写的,不要改成驼峰,否则会调用不到方法。如果你要在自定义播放器里屏蔽某一种手势,需要重写onTouchEvent并拦截对应方向的事件。

调整比例通过setVideoScaleType控制:

videoPlayer.setVideoScaleType(GSYVideoPlayer.SCREEN_MATCH_DEFAULT); // 等比缩放,可能有黑边 videoPlayer.setVideoScaleType(GSYVideoPlayer.SCREEN_MATCH_FULL); // 拉伸铺满,可能变形 videoPlayer.setVideoScaleType(GSYVideoPlayer.SCREEN_MATCH_16_9); // 强制 16:9

实际使用中,竖屏短视频用SCREEN_MATCH_DEFAULT,横屏长视频用SCREEN_MATCH_FULL或按视频宽高比动态计算。多分辨率切换时,先记录当前播放进度,再setUp新的清晰度地址,prepared 后再seekTo到记录位置。切换期间用户看到的是黑屏或加载动画,所以分辨率切换会放在片头广告结束、正片即将播放的时间点。

5. 弹幕、外挂字幕、滤镜与 GIF 截图:播放器之上的渲染能力

5.1 弹幕与外挂字幕:与播放进度保持同步

弹幕不是播放器的功能,而是覆盖在播放器上的一层独立 View。GSYVideoPlayer 提供的绑定接口简化了弹幕层与播放器的联动:

DanmakuView danmakuView = findViewById(R.id.danmakuView); player.setDanmakuView(danmakuView);

setDanmakuView会在播放器暂停时暂停弹幕、恢复时继续弹幕、seek 时自动清除当前弹幕并重新定位。如果你不用这个接口,就要自己在播放器的onPauseonResumeonSeekComplete回调里手动调用danmakuView.pause()danmakuView.start()danmakuView.seekTo(),很容易漏掉某个状态导致弹幕和视频画面错位。

外挂字幕最常见的做法是解析 srt 文件,然后监听播放进度更新字幕文本:

List<SubtitleItem> items = SrtParser.parse(subtitleFile); player.setOnProgressListener((newProgress, position, duration) -> { SubtitleItem item = SrtParser.find(items, position); subtitleView.setText(item == null ? "" : item.text); });

字幕解析需要处理时间戳跨行、逗号还是点号的小数分隔符、以及字幕重叠的情况。position的单位是 millis,srt 文件的时间戳格式是HH:mm:ss,mmm,解析时统一转成 millis 比对。如果还要支持 ass 字幕,就需要额外解析样式标签{\an8}{\pos(x,y)}这类指令,业务上不追求完全还原 ass 特效的话,建议直接剥掉花括号里的内容,只保留纯文本。

5.2 滤镜与水印:基于 OpenGL 的渲染管线

滤镜的前提是播放器工作在其他视频内核之上,并且开启了纹理渲染模式。没有enableMediaCodecTexture()的情况下,setVideoFilter不会生效。打开纹理渲染后,视频帧被解码进 GPU 纹理,再经过一长串 GPUImage Filter 输出到屏幕:

GSYVideoType.enableMediaCodecTexture(); player.setVideoFilter(new GPUImageBrightnessFilter(0.5f));

GPUImageBrightnessFilter只是亮度示例,常见滤镜还包括对比度、饱和度、色彩偏移、模糊、素描效果。滤镜可以叠加,但每多一个 filter 就多一次 fragment shader 执行,低端机上要控制滤镜数量,否则帧率下降明显。

水印本质上也是滤镜。比较通用的做法是自定义一个继承GPUImageTwoInputFilter的 Filter,第二个输入纹理就是水印 Bitmap,在 fragment shader 里把水印纹理通过 alpha 混合叠加到视频纹理上。水印位置需要在 shader 里根据纹理坐标计算,左上角还是右上角取决于坐标系的 origin。设置水印时要注意 Bitmap 不要频繁创建和释放,因为纹理上传 GPU 的带宽有限,每次重建水印纹理都会引起掉帧。

5.3 GIF 截图与片头、中间广告

GIF 截图的核心是从播放器取出连续帧,再编码成 GIF。单帧截图可以直接调播放器接口:

player.saveFrame(new File(getCacheDir(), "frame.png").getAbsolutePath());

saveFrame从当前帧所在的纹理或 Surface 中读取像素数据,时机必须是在视频绘制完成之后,调早了拿到的可能是黑屏。GIF 则是连续抓取多帧:

// 常见做法:按固定间隔抓帧,再交给 GIF 编码器 AnimatedGifEncoder encoder = new AnimatedGifEncoder(); encoder.start(outputStream); encoder.setDelay(80); // 每帧间隔 80ms encoder.setRepeat(0); // 无限循环 for (Bitmap frame : frames) { encoder.addFrame(frame); } encoder.finish();

GIF 的帧率不要超过 15fps,分辨率也要等比压缩,常见的 3 秒 GIF 在 480x270 下编码出来也就几百 KB。抓帧过程不能放在 UI 线程,因为 bitmap 的像素读取和 PNG/JPEG 编码都是 CPU 密集型操作。

片头广告和中间广告在 GSYVideoPlayer 的示例代码里有现成的GSYADVideoPlayer实现。它的逻辑是先播广告源,广告播放完成后再加载正片:

GSYADVideoPlayer adPlayer = findViewById(R.id.adPlayer); adPlayer.setUp(adUrl, false, null); adPlayer.startPlayLogic(); adPlayer.setOnCompletionListener(mp -> { // 广告结束,切换正片 videoPlayer.setUp(videoUrl, true, cacheKey); videoPlayer.startPlayLogic(); });

中间广告的插入位置通常在正片暂停时或某一个进度节点。实现时要记录正片当前播放位置,广告结束后从记录位置继续 seek,不能让用户重新看一遍广告前后的内容。片头广告建议复用同一个播放器容器,避免两个播放器实例抢音频焦点。

5.4 多个同时播放:音频焦点与 SurfaceTexture 上限

GSYVideoPlayer 默认的 GSYVideoManager 是单实例设计,同一时间控制一个播放器,避免多个视频抢占音频焦点和 GL 资源。如果你需要「多个同时播放」,比如双路对比视频,就要绕过 manager 直接创建多个 player 实例,此时有两道硬限制:

资源限制说明
SurfaceTexture不同机型支持的数量上限不同,一般只有 2 到 4 个
AudioFocus只能同时有一个音频输出,多路播放时其他路必须 mute

SurfaceTexture 超限通常表现为黑屏或不渲染,没有明显的异常日志,需要自行控制实例数量。多路播放时,非主视频需要用player.setVolume(0f, 0f)静音,再配合AudioManager.requestAudioFocus处理来电、闹钟等系统打断场景。不要用setVolume(0, 0)的旧式写法,GSYVideoPlayer 的对外 API 基于 float 音量。

6. rtsp 直播源接入:TCP/UDP 与延迟调优的技巧

6.1 为什么 rtsp 只推荐 IJKplayer

rtsp 拉流走的是实时传输协议,ExoPlayer 对 rtsp 的支持范围还很有限,MediaPlayer 基本不碰这个协议。GSYVideoPlayer 里做 rtsp 直播源调试,几乎都是切到 IJKplayer 内核。如果你没有公网源,可以用 GStreamer 自带的 rtsp server 在本地起一路测试流,推一路摄像头或文件源,再拿 Android 设备拉流验证,这比找不稳定的公网地址要可靠得多。

6.2 rtsp_transport 与延迟参数

rtsp 默认传输方式在不同的服务端实现里不一样,UDP 在丢包严重的 WiFi 环境下会花屏,TCP 则相对稳定。常见做法是强制走 TCP:

IjkMediaPlayer ijkPlayer = (IjkMediaPlayer) player; // 强制 TCP 传输,减少 UDP 丢包导致的马赛克 ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp"); // 让 rtsp 解析时优先选择 TCP ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp"); // 只拉视频流,音频单独处理时效果更好 ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video");

rtsp_transport取值有udptcpudp_multicast。内网环境 UDP 延迟更低,公网和弱网环境 TCP 更稳定。直播延迟调优时,把analyzedurationprobesize压到最小,再配合:

ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer"); ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "packet-buffering", "0");

fflagsnobuffer告诉 FFmpeg 不要额外缓冲,包到达后尽量直接解码显示;packet-buffering=0关闭包缓冲。这两个参数能明显降低延迟,但网速波动时卡顿也会更频繁,属于典型的「用稳定性换低延迟」取舍。

6.3 无缝切台与缓存开关

rtsp 是直播流,没有文件总长度,不能 seek,也不能走边播边缓存。切台时如果调用release再重新setUp,中间会有明显的黑屏和 loading。无缝切台的思路是预加载:

// 当前频道还在播时,提前创建新的播放器实例去 prepare 下一路 IjkMediaPlayer nextPlayer = new IjkMediaPlayer(); nextPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp"); nextPlayer.setDataSource(nextUrl); nextPlayer.prepareAsync(); // 用户点击切台后,把 nextPlayer 的 Surface 绑定到当前播放器的显示层

切台瞬间把当前播放器释放,用已经准备好的下一路接管 Surface,视觉上几乎没有卡顿。注意切台后的rtsp_transport要和上一路保持一致,否则播放器重新协商传输方式,预加载效果会被抵消。直播场景统一的配置是setUp(url, false, null)再显式setCacheWithPlay(false),否则缓存代理会把无限长的直播流一直写盘。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 9:46:31

Colibri:面向MoE架构的C语言轻量级推理引擎

1. 项目概述&#xff1a;Colibri 是什么&#xff0c;它解决的到底是什么问题&#xff1f;Colibri 这个名字乍一听像某种蜂鸟——轻盈、敏捷、高代谢。放在当前大模型推理的语境下&#xff0c;它恰恰就是这个名字的具象化&#xff1a;一个用C 语言实现的、专为MoE&#xff08;Mi…

作者头像 李华
网站建设 2026/9/16 9:45:19

TVA具身架构驱动的机器行为与人类价值对齐机制

前沿技术探索:TVA智能体(简称TVA) TVA智能体(亦称“AI智能体视觉”或“TVA视觉智能体”)是依托Transformer架构与“因式智能体”理论构建的通用视觉技术体系。它有机融合深度强化学习(DRL)、卷积神经网络(CNN)与因式分解算法(FRA),构成了具身智能的核心视觉中枢(…

作者头像 李华
网站建设 2026/9/16 9:44:52

结构动力学仿真技术:原理、应用与发展趋势

1. 结构动力学仿真技术概述结构动力学仿真作为工程分析领域的重要分支&#xff0c;主要研究结构在动态载荷作用下的响应特性。这项技术通过建立数学模型&#xff0c;模拟结构在实际工况中的振动、冲击、疲劳等行为&#xff0c;为工程设计提供关键数据支持。现代结构动力学仿真已…

作者头像 李华
网站建设 2026/9/16 9:43:19

【2016-02-23】Linux无线模块WEXT简单分析

[历史归档] 本文原发布于 cstriker1407.info 个人博客&#xff0c;内容为历史存档&#xff0c;仅供参考。 发布时间&#xff1a; 2016-02-23 &#xff5c; 标题&#xff1a;Linux无线模块WEXT简单分析 &#xff5c; 分类&#xff1a; 操作系统 / linux / kernel &#xff…

作者头像 李华
网站建设 2026/9/16 9:43:01

固定资产新增API实战:从表设计到接口联调全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 9:42:49

新闻资讯微信小程序毕设开发全攻略:从选题到答辩避坑指南

每年到毕设季&#xff0c;都会有大量同学来问同一个问题&#xff1a;做什么题目既好过、又能写进简历&#xff0c;最好还能拿出来演示不丢人。我做了这么多年开发&#xff0c;也带过不少实习生&#xff0c;实话实说&#xff0c;新闻资讯微信小程序这类题目始终是稳妥之选。它不…

作者头像 李华