GSYVideoPlayer 如何在 Compose Native 模式自绘播放控制层并订阅 events 与 stateFlow
2026/9/13 2:34:41 网站建设 项目流程

GSYVideoPlayer 如何在 Compose Native 模式自绘播放控制层并订阅 events 与 stateFlow

【免费下载链接】GSYVideoPlayerVideo players (IJKplayer, ExoPlayer, MediaPlayer), HTTPS, 16k page size, danmaku (bullet chat) support, external subtitles, support for filters, watermarks, and GIF screenshots, pre-roll and mid-roll ads, multiple simultaneous playback, basic seeking/dragging, volume and brightness adjustment, play-while-cache support项目地址: https://gitcode.com/GitHub_Trending/gs/GSYVideoPlayer

当业务要求播放 UI 完全由 Compose 重绘、但仍要复用 GSYVideoPlayer 的多内核与渲染管线时,gsyVideoPlayer-compose模块的 Native 模式(GSYPlayerController+GSYPlayerSurface)是文档给出的对应路径:画面由GSYPlayerSurface承载(内部是AndroidView中的隐藏 UI host player),控制层则用任意 Compose 组件自绘;播放状态不再走 callback,而是通过controller.snapshot(ComposeState)、controller.stateFlow(StateFlow)和controller.events(SharedFlow 边沿事件)三条通道订阅。本文按「引入依赖 → 搭最小 Native 屏幕 → 自绘控制层 → 验证与约束」的顺序把这条路径走通。

适用前提(来自 doc/COMPOSE_USE.md):

  • 模块版本 v13.1.0 起随主版本发布,外部坐标io.github.carguo:gsyvideoplayer-compose:13.1.0
  • 环境要求:minSdk ≥ 23、Kotlin 2.0.x、AGP 8.6+、JDK 17;
  • 模块内部已通过compose-bom 2024.06.00统一 Compose 依赖版本,并使用 Compose Compiler Gradle Plugin,无需再单独指定composeCompilerVersion

一、引入依赖

// 方式 A:Maven Central / GitHub Packages implementation "io.github.carguo:gsyvideoplayer-compose:13.1.0" // 方式 B:本仓库内源码依赖(在仓库内开发/调试时用) implementation project(":gsyVideoPlayer-compose")

模块本身已api依赖gsyVideoPlayer-java,无需重复引入;如果你要用 EXO/Ali 内核,还需按原 GSY 的方式额外引入对应坐标。

二、Native 模式的三个构件

构件职责
rememberGSYPlayerController(url, autoPlay, ...)创建并记忆GSYPlayerController;传了url时内部以LaunchedEffect执行controller.setUp(url, ...);离开 Composition 时自动dispose()
GSYPlayerSurface(controller, modifier)纯渲染层:只承载画面,不绘制任何控制 UI,可在其上叠加任意 Compose 控制层
controller.snapshot/stateFlow/events状态订阅三通道:State<GSYPlayerSnapshot>(Compose 渲染用)、StateFlow<GSYPlayerSnapshot>(协程/ViewModel 用)、SharedFlow<GSYPlayerEvent>(一次性边沿事件)

如果不想自绘,顶层封装GSYComposePlayer(controller, showDefaultControls = true)一行就能得到「画面 + 默认控制条」。自绘控制层的做法就是不用它,自己在同一个Box里把GSYPlayerSurface和你的控件 Composable 叠在一起(参考模块结构见 GSYDefaultControls.kt 顶部注释)。

三、步骤 1:建 controller、挂 Surface、接上两条订阅

以下代码与 doc/COMPOSE_USE.md 第三节示例及 FullFeatureNativeActivity.kt 的写法一致;url使用仓库 demo 数据中的测试地址(DemoVideoUrls.java 的SAMPLE_GSY),替换成自己的视频地址即可:

import android.util.Log import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.shuyu.gsyvideoplayer.compose.native_.GSYPlayerEvent import com.shuyu.gsyvideoplayer.compose.native_.GSYPlayerSurface import com.shuyu.gsyvideoplayer.compose.native_.rememberGSYPlayerController import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map // DemoVideoUrls.SAMPLE_GSY,仓库 Compose Demo 共用的测试样片 private const val URL = "https://res.exexm.com/cw_145225549855002" @Composable fun NativeCustomControlsScreen() { // 传 url 即自动 setUp;autoPlay = true 表示 attach 完成后自动 startPlayLogic val controller = rememberGSYPlayerController(url = URL, autoPlay = true) val snap by controller.snapshot // 1) 边沿事件:一个 LaunchedEffect 即可处理多种事件,无需多个 setter LaunchedEffect(controller) { controller.events.collect { event -> when (event) { is GSYPlayerEvent.Prepared -> Log.d("GSY", "已准备就绪(此时可取 duration/videoSize)") is GSYPlayerEvent.Error -> Log.e("GSY", "播放失败 what=${event.what} extra=${event.extra}") GSYPlayerEvent.AutoComplete -> Log.d("GSY", "播放完成") else -> Unit } } } // 2) StateFlow 形态:适合在 ViewModel / UseCase 里 collect LaunchedEffect(controller) { controller.stateFlow .map { it.isPlaying } .distinctUntilChanged() .collect { isPlaying -> Log.d("GSY", "isPlaying=$isPlaying") } } // 3) 画面层;自绘控制层叠加在它之上(见下一节) Box(Modifier.fillMaxSize()) { GSYPlayerSurface(controller, Modifier.matchParentSize()) } }

GSYPlayerSnapshot的字段(GSYPlayerState.kt):stateIdle/Preparing/Playing/Buffering/Paused/Completed/Error,与内核GSYVideoView.CURRENT_STATE_*一一对应)、currentPositiondurationbufferPercentisPlayingvideoWidth/videoHeight,以及netSpeedisCacheReadyspeedisLocked等扩展字段。

四、步骤 2:自绘控制层

完整可运行参照是 CustomControlsThemeComposeActivity.kt(Demo 列表中第 24 项「自定义主题 Controls」):渐变浮层 + 自定义播放按钮 +Slider拖拽 seek + 点画面切换浮层显隐,完全用 Compose 控件替代GSYDefaultControls。其骨架如下(url替换为自己的地址):

@Composable fun ScreenWithOwnControls(url: String) { val controller = rememberGSYPlayerController() val snap by controller.snapshot var overlayVisible by remember { mutableStateOf(true) } var dragging by remember { mutableStateOf(false) } var dragValue by remember { mutableFloatStateOf(0f) } // 用完整 builder 配置(与 Java 端链式 builder 相同的一套 GSYVideoOptionBuilder) LaunchedEffect(Unit) { val builder = GSYVideoOptionBuilder() .setUrl(url) .setVideoTitle("Custom Controls Demo") .setIsTouchWiget(false) // 内核自带的手势 UI 关闭,手势交给自绘层 controller.setUp(builder, autoPlay = true) } Box( modifier = Modifier .fillMaxWidth() .aspectRatio(16f / 9f) .background(Color.Black) .pointerInput(Unit) { detectTapGestures(onTap = { overlayVisible = !overlayVisible }) } ) { GSYPlayerSurface(controller = controller, modifier = Modifier.fillMaxSize()) // 自绘控制层:全部由 snap 驱动 if (overlayVisible) { Box(Modifier.fillMaxSize()) { // 中央播放/暂停按钮 Box( Modifier.align(Alignment.Center), contentAlignment = Alignment.Center ) { Box( Modifier .size(64.dp) .clip(CircleShape) .background(Color.White.copy(alpha = 0.85f)) .pointerInput(Unit) { detectTapGestures(onTap = { controller.togglePlayPause() }) } ) } // 底部进度条:拖拽中只更新本地预览,抬手才一次性 seekTo val maxMs = snap.duration.coerceAtLeast(1L).toFloat() Slider( value = if (dragging) dragValue else snap.currentPosition.toFloat().coerceIn(0f, maxMs), onValueChange = { dragging = true; dragValue = it }, onValueChangeFinished = { controller.seekTo(dragValue.toLong()) // 抬手提交,避免每帧 seek 卡顿 dragging = false }, valueRange = 0f..maxMs, modifier = Modifier.align(Alignment.BottomCenter) ) Text( "${snap.currentPosition}/${snap.duration} ms · buffer ${snap.bufferPercent}%", color = Color.White, modifier = Modifier.align(Alignment.BottomStart) ) } } } }

几点与自绘直接相关的取舍(均来自源码注释与 demo 实现):

  • seek 的提交时机:模块自带的 GSYDefaultControls.kt 同样是「拖拽期间只改本地预览状态、onValueChangeFinished才一次性controller.seekTo」,自绘层照此实现即可,避免每帧 seek 造成卡顿。
  • 需要内核尚未封装的能力(字幕、镜像、截图、滤镜等)时用controller.withHost { player -> ... }逃生口,例如player.setSubTitle(...)。两个硬约束:必须在主线程调用;禁止在 block 里调player.setVideoAllCallBack(...)——那会把 controller 内部稳定的 dispatcher 顶掉,导致events/setOnXxx/setUserVideoAllCallBack全部失效。需要用户级VideoAllCallBack时改用controller.setUserVideoAllCallBack(callback),它与内部 dispatcher 链式分发、互不覆盖。
  • 全屏controller.enterFullscreen(activity, hideActionBar, hideStatusBar)由 GSY 内核接管(等价 Java 版startWindowFullscreen),期间内核会克隆 host 接管渲染,events照常 emitGSYPlayerEvent.EnterFull/QuitFullexitFullscreen(activity)返回false表示当前并不在全屏态。参考 ListWithFullscreenActivity.kt。

五、结果验证

仓库 app 模块的入口Compose Demo(ComposeDemoListActivity.kt)汇总了 24 个可运行的 Compose Activity,其中与本文任务直接相关的是:

入口验证点
8 · Full-Feature NativeFullFeatureNativeActivity.kt完整自绘控件:速率 / Seek / 错误 Retry / 完成态,LaunchedEffect里 collectevents处理Prepared/Error
24 · 自定义主题 ControlsCustomControlsThemeComposeActivity.kt完全自绘控件替代默认控制条:渐变浮层 + Slider seek + 多主题
7 · Detail NativeDetailNativeActivity.ktNative 模式详情页形态

按以下信号判断接入是否正确(均为源码注释与文档明确给出的行为,不是固定日志):

  • 起播后stateFlow/snapshotisPlaying变为truestate经过Preparing进入Playing(缓冲时是Buffering);
  • 流元信息就绪时events发出GSYPlayerEvent.Prepared(此时可获取 duration/videoSize 等);
  • 拖动滑块松手后 seek 生效,events会发出SeekCompletesnapshot.currentPosition随播放推进(snapshot 为 500ms 低频轮询同步,进度条可能略有滞后属正常);
  • 播放失败时events发出GSYPlayerEvent.Error(what, extra),携带原始MediaPlayer.OnError的 what/extra,据此在自绘层显示重试入口(demo 中重试调controller.retry())。

注意events的边沿语义:replay = 0extraBufferCapacity = 16、溢出策略DROP_OLDEST(见 GSYPlayerController.kt)。订阅前发生的事件不会重放,错过即错过;所以「是否正在播放、播到哪了」这类连续状态要读stateFlow/snapshot,不要试图从events里还原状态。

六、约束与易错点

  • 旧版setOnError/setOnComplete/setOnPrepared已标记@Deprecated(保留可用),新代码推荐events+stateFlow
  • rememberGSYPlayerControllerautoPauseResume默认为true:宿主ON_PAUSE自动GSYVideoManager.onPause()ON_RESUME自动恢复;悬浮窗 / PiP 等需要后台持续播放的业务显式传false
  • 离开 Composition 时 controller 自动dispose(),之后所有 setter /setUp均为 no-op;withHost在 released 后直接返回null
  • snapshotstateFlow同源同值,仅 commit 时机略有差异:Compose snapshot 在下一帧提交,StateFlow 立即可见。
  • 内核切换与原版一致、不受 Compose 模块影响:PlayerFactory.setPlayManager(Exo2PlayerManager::class.java)等按原 GSY 方式配置即可(EXO 多源切换可参考 ExoSwitchSourceComposeActivity.kt)。
  • 能力缺口与已知问题的完整清单见 doc/COMPOSE_BACKLOG.md;「何时选 Native、何时选 Wrapper」的决策矩阵在 doc/COMPOSE_USE.md 第九节——简言之:需要 Compose 原生 UI 流式驱动选 Native,存量 Java/XML 业务想最快平移选 Wrapper。

【免费下载链接】GSYVideoPlayerVideo players (IJKplayer, ExoPlayer, MediaPlayer), HTTPS, 16k page size, danmaku (bullet chat) support, external subtitles, support for filters, watermarks, and GIF screenshots, pre-roll and mid-roll ads, multiple simultaneous playback, basic seeking/dragging, volume and brightness adjustment, play-while-cache support项目地址: https://gitcode.com/GitHub_Trending/gs/GSYVideoPlayer

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

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

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

立即咨询