doc/COMPOSE_USE.md
当前状态:v13.1.0 起随主版本发布
gsyVideoPlayer-compose模块已纳入 Maven Central / GitHub Packages 发布流。外部项目可直接使用io.github.carguo:gsyvideoplayer-compose:13.1.0;在本仓库内开发或调试时,仍可使用implementation project(":gsyVideoPlayer-compose")的源码依赖方式。当前能力缺口、已知问题与分轮推进路线图已归档到 doc/COMPOSE_BACKLOG.md;后续每一轮代码与 demo 推进都会同步更新该文件。
新增模块:gsyVideoPlayer-compose,提供两种使用方式:
| 模式 | 适用场景 | 使用入口 |
|---|---|---|
| Wrapper(AndroidView 包装) | 已用 GSY,想快速塞进 Compose 屏;保留全屏、手势、缓存、字幕、滤镜等全部能力 | GSYVideoPlayerView { ... } |
| Native(Compose 原生控件层) | 想完全用 Compose 重绘控制 UI,但仍复用 GSY 多内核与渲染管线 | GSYComposePlayer + GSYPlayerController |
模块基于 gsyVideoPlayer-java,不修改任何旧代码。
// 方式 A:Maven Central / GitHub Packages
implementation "io.github.carguo:gsyvideoplayer-compose:13.1.0"
// 方式 B:本仓库内源码依赖
implementation project(":gsyVideoPlayer-compose")
模块本身已 api 依赖 gsyVideoPlayer-java,无需重复引入;但如果你需要 EXO/Ali 内核,还需按照原有方式额外引入对应坐标。
要求:
minSdk ≥ 23、Kotlin 2.0.x、AGP 8.6+、JDK 17。模块内部已通过compose-bom 2024.06.00统一 Compose 依赖版本,并使用 Compose Compiler Gradle Plugin(无需再单独指定composeCompilerVersion)。
最快的接入方式,完全等价于在 XML 里放一个 StandardGSYVideoPlayer。
@Composable
fun MyVideoScreen(url: String) {
GSYVideoPlayerView(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f),
setUp = { player ->
GSYVideoOptionBuilder()
.setUrl(url)
.setCacheWithPlay(true)
.setVideoTitle("Compose Demo")
.setIsTouchWiget(true)
.build(player)
player.startPlayLogic()
},
// 默认会自动桥接 onPause/onResume 与离屏 release
autoPauseResume = true,
autoReleaseOnDispose = true,
)
}
特性:
Lifecycle.Event.ON_PAUSE → GSYVideoManager.onPause()、ON_RESUME → onResume()release()(可关闭)如果需要使用其它 NormalGSYVideoPlayer 子类(例如 ListGSYVideoPlayer、SampleControlVideo),用通用版本:
GSYAnyVideoPlayerView(
modifier = Modifier.fillMaxWidth().aspectRatio(16f / 9f),
factory = { ctx -> SampleControlVideo(ctx) },
setUp = { player ->
GSYVideoOptionBuilder().setUrl(url).build(player)
player.startPlayLogic()
},
)
setUpKey:参数变化时如何让 setUp 重跑setUp 默认只在 factory 阶段调用一次。这是有意设计:每次 recomposition 都执行
builder.build(player) 会重置 url / 重启播放,反而是 bug 而非 feature。
如果你想让 url 等参数变化时主动重新执行 setUp,传 setUpKey:
GSYVideoPlayerView(
setUp = { player -> GSYVideoOptionBuilder().setUrl(url).build(player); player.startPlayLogic() },
setUpKey = url, // url 变化时再次调用 setUp,否则 update 阶段是 no-op
)
setUpKey == null(默认):兼容老用法,AndroidView.update 不做任何事。
UI 完全由 Compose 自绘,画面依然由 TextureView(在 AndroidView 中)承载,内核走 IPlayerManager。
@Composable
fun MyNativeScreen(url: String) {
val controller = rememberGSYPlayerController(url = url, title = "Native Demo")
GSYComposePlayer(
controller = controller,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f),
showDefaultControls = true,
)
}
rememberGSYPlayerController 会在离开 Composition 时自动 release()。
val controller = rememberGSYPlayerController(url = url)
val snap by controller.snapshot
Box(modifier = Modifier.fillMaxSize()) {
GSYPlayerSurface(controller, Modifier.matchParentSize()) // 画面
MyOwnControls(snap, controller, Modifier.matchParentSize())// 自绘控制层
}
GSYPlayerController:
controller.setUp(builder: GSYVideoOptionBuilder)
controller.setUp(url, cacheWithPlay = false, title = "")
controller.play()
controller.togglePlayPause()
controller.pause()
controller.resume()
controller.seekTo(positionMs: Long)
controller.setSpeed(1.5f, soundTouch = true)
controller.release()
// 全屏(R2 起新增,由 GSY 内核接管,等价于 Java 版 startWindowFullscreen / backFromFull)
controller.enterFullscreen(activity, hideActionBar = true, hideStatusBar = true)
controller.exitFullscreen(activity) // 返回 Boolean,false 表示当前并不在全屏态
val inFs: Boolean = controller.isFullscreen // 直读底层标志位
// 用户级 VideoAllCallBack(与内部 dispatcher 链式分发,不再覆盖 events)
controller.setUserVideoAllCallBack(object : GSYSampleCallBack() {
override fun onClickStartIcon(url: String?, vararg objects: Any?) { /* 用户埋点 */ }
})
// 逃生口:直接拿底层 StandardGSYVideoPlayer 调用尚未封装的方法(必须主线程;release 后 no-op)
controller.withHost { player ->
player.setSubTitle("https://example.com/sub.srt") // 字幕
player.setMirrorRotation(true) // 镜像
player.taskShotPic { bmp -> /* 截图回调 */ } // 快照
}
val snap: State<GSYPlayerSnapshot> = controller.snapshot
// snap.value -> GSYPlayerSnapshot(state, currentPosition, duration,
// bufferPercent, isPlaying, videoWidth, videoHeight)
GSYPlayState 状态机与原生 GSYVideoView.CURRENT_STATE_* 一一对应:
Idle Preparing Playing Buffering Paused Completed Error
⚠️ 关于
withHost { ... }:这是为了在能力对齐补齐之前给业务一个逃生口(Escape Hatch), 不是推荐路径。禁止在 block 里调player.setVideoAllCallBack(...)——会把内部 dispatcher 顶掉,导致events/setOnXxx/setUserVideoAllCallBack全部失效。 需要回调请改用setUserVideoAllCallBack入口。
自 v13.x 起,
GSYPlayerController提供与 Coroutine 完全对齐的响应式接口。 旧版setOnError / setOnComplete / setOnPrepared仍保留但已标记@Deprecated, 推荐切换到events(边沿事件)+stateFlow(连续状态)。
import com.shuyu.gsyvideoplayer.compose.native_.GSYPlayerEvent
val controller = rememberGSYPlayerController(url = url, autoPlay = true)
// 1) 一次性"边沿事件"流:onPrepared / onAutoComplete / onPlayError / onEnterFullscreen / onQuitFullscreen
LaunchedEffect(controller) {
controller.events.collect { event ->
when (event) {
is GSYPlayerEvent.Prepared -> Log.d("Demo", "已准备就绪")
is GSYPlayerEvent.AutoComplete -> Log.d("Demo", "播放完成")
is GSYPlayerEvent.Error -> Log.e("Demo", "播错 what=${event.what} extra=${event.extra}")
GSYPlayerEvent.EnterFull -> fullscreen = true // 由内核接管全屏后回调
GSYPlayerEvent.QuitFull -> fullscreen = false
}
}
}
// 2) 状态读取——StateFlow 形态,便于在 ViewModel / UseCase 里 collect
LaunchedEffect(controller) {
controller.stateFlow
.map { it.isPlaying }
.distinctUntilChanged()
.collect { isPlaying -> /* ... */ }
}
// 3) Compose 直接渲染——仍可用 controller.snapshot
val snap: GSYPlayerSnapshot by controller.snapshot
设计取舍:
events 默认 replay = 0、extraBufferCapacity = 16、溢出 DROP_OLDEST,
emit 不会阻塞、订阅前发生的事件不会重放。由 GSYVideoPlayerView 自动处理;如需手动控制,传 autoPauseResume = false 并自己用 LifecycleEffect。
全屏可直接调用底层 player.startWindowFullscreen(...),与传统用法一致。
rememberGSYPlayerController 会在离开 Composition 时自动 release。
全屏(推荐路径,自 R2 起)——由 GSY 内核接管,与 Java 版 startWindowFullscreen 走同一管线(反射克隆 host 接管渲染、自动旋转、自动隐藏系统栏):
val controller = rememberGSYPlayerController(url = url, autoPlay = true)
val activity = LocalContext.current as Activity
var fullscreen by remember { mutableStateOf(false) }
LaunchedEffect(controller) {
controller.events.collect { ev ->
when (ev) {
GSYPlayerEvent.EnterFull -> fullscreen = true
GSYPlayerEvent.QuitFull -> fullscreen = false
else -> {}
}
}
}
BackHandler(enabled = fullscreen) { controller.exitFullscreen(activity) }
Button(onClick = { controller.enterFullscreen(activity) }) { Text("全屏") }
不再推荐
Dialog(...) + 手动 requestedOrientation自绘全屏——两个 demo DetailNativeActivity 与 ListWithFullscreenActivity 已切换到上述路径,可作为参考。
与原版完全一致,不受 Compose 模块影响:
PlayerFactory.setPlayManager(Exo2PlayerManager::class.java)
CacheFactory.setCacheManager(ExoPlayerCacheManager::class.java)
App 模块下入口 Compose Demo(ComposeDemoListActivity.kt)汇总了 24 个可运行的 Compose Activity,外加 1 份共享测试数据:
表中第 25 行的
DemoSamples.kt是data object(与 Java/XML Demo 共用同一组测试 URL),并非可运行 Activity,仅为方便溯源附在表尾。
| # | 名称 | 入口 | 说明 |
|---|---|---|---|
| 1 | Basic Wrapper(ΔD1 已升级) | BasicWrapperActivity.kt | GSYVideoPlayerView 最小用法 + 5 个高频 builder 选项(setSeekRatio / setShowPauseCover / setReleaseWhenLossAudio / setStartAfterPrepared / setVideoAllCallBack)实时演示 |
| 2 | Switch URL(ΔD5 已升级) | SwitchUrlActivity.kt | Wrapper 模式下切流;KDoc see-also → D8 无缝切换 |
| 3 | Multi-Window(ΔD6 已升级) | MultiWindowActivity.kt | 多个播放器同屏共存(互斥版);指引 → D7 真并行版 |
| 4 | List Play (Native)(ΔD4 已升级) | ListPlayNativeActivity.kt | LazyColumn + Native Composable;离屏 setUp 重置 vs pause Switch + setShowPauseCover Switch + Compose 自绘占位封面 |
| 5 | Auto-Play List(ΔD7 已升级) | AutoPlayListActivity.kt | 滚动可视区自动播放/释放;KDoc 解释 surface 接管取舍 |
| 6 | List with Fullscreen | ListWithFullscreenActivity.kt | 列表内层级式全屏 |
| 7 | Detail Native | DetailNativeActivity.kt | Native 模式详情页 |
| 8 | Full-Feature Native | FullFeatureNativeActivity.kt | 速率 / Seek / 错误 / 完成态 |
| # | 名称 | 入口 | 说明 |
|---|---|---|---|
| 9 | Native 滤镜 | DetailFilterComposeActivity.kt | 通过 withHost 注入 setEffectFilter,循环切换 6 种 GLSL 滤镜 |
| 10 | Native 缓存 / 下载 | CacheDownloadComposeActivity.kt | ProxyCacheManager 代理 + isCacheReady 状态 + 清缓存 |
| 11 | Native 字幕 | SubtitleComposeActivity.kt | 3 字幕源切换 + 字号 / 开关,IJK 内核 |
| 12 | Native Seamless 切换(D8) | SwitchSeamlessComposeActivity.kt | 同一 controller 跨 list/detail 复用,不重 setUp 不重拉流 |
| 13 | Native 前贴片广告 | AdInListComposeActivity.kt | 单 controller AD → AutoComplete → 切正片,演示 events 边沿事件链 |
| 14 | Native Compose 自绘弹幕 | DanmakuComposeActivity.kt | Canvas + textMeasurer 与 snapshot.currentPosition 同步 |
| 15 | Native EXO 多源切换 | ExoSwitchSourceComposeActivity.kt | PlayerFactory.setPlayManager(Exo2PlayerManager) + MP4/HLS + 5 档倍速 |
| 16 | Wrapper 真并行多窗口 | MultiWindowParallelComposeActivity.kt | MultiSampleVideo + CustomManager,3 个并行播放(非互斥) |
| # | 名称 | 入口 | 说明 |
|---|---|---|---|
| 17 | 竖屏短视频 (VerticalPager) | VerticalShortVideoComposeActivity.kt | VerticalPager + 单 controller 跨页 setUp,循环播放 |
| 18 | 悬浮窗(画中画) | FloatingWindowComposeActivity.kt | SYSTEM_ALERT_WINDOW 拉起 FloatPlayerView,跨 Activity 常驻 |
| 19 | 多类型列表 | MoreTypeComposeActivity.kt | LazyColumn 多 type cell(Normal/Ad/Cover/Unknown) |
| 20 | 图文混排(视频 + WebView) | WebDetailComposeActivity.kt | 上方 Compose 播放器 + 下方 AndroidView WebView 双栈共存 |
| 21 | 纯音频播放 | AudioOnlyComposeActivity.kt | raw 资源 + enableRawPlay;Compose 端 controller 仍要挂一个 GSYPlayerSurface(host 必须有 Surface 载体,1dp 隐身节点即可) |
| 22 | 自定义 URL / 本地文件 | LocalFileComposeActivity.kt | URL 输入 + cache 切换 + raw:// / http:// 多种源类型 |
| 23 | MediaCodec 硬解切换 | MediaCodecComposeActivity.kt | GSYVideoType.enableMediaCodec() / disableMediaCodec() 实时切换 |
| 24 | 自定义主题 Controls | CustomControlsThemeComposeActivity.kt | Compose 自绘控件取代 GSYDefaultControls:渐变浮层 + Slider seek + 多主题切换 |
| # | 名称 | 入口 | 说明 |
|---|---|---|---|
| 25 | Demo 数据 | DemoSamples.kt | 复用与 Java/XML Demo 同一组测试 URL(data object,非可运行 Activity) |
gsyVideoPlayer-compose/build.gradle 已 apply 了:
gradle/lib.gradle —— 标准 com.android.library + singleVariant('release') { withSourcesJar(); withJavadocJar() }gradle/publish.gradle —— 发布到 GitHub Packages(com.shuyu:gsyvideoplayer-compose)gradle/maven-central-publish.gradle —— 发布到 Maven Central(io.github.carguo:gsyvideoplayer-compose)仓库根目录打 vX.Y.Z tag,触发 .github/workflows/publish-maven-central.yml,会一并发布所有库模块(含 compose):
io.github.carguo:gsyvideoplayer-compose:<PROJ_VERSION>
也可手动执行:
./gradlew :gsyVideoPlayer-compose:publishMavenCentralPublicationToSonatypeRepository \
closeAndReleaseSonatypeStagingRepository \
-PPUBLISH_TARGET=mavenCentral
打 tag(任意名)触发 .github/workflows/release.yml:
com.shuyu:gsyvideoplayer-compose:<PROJ_VERSION>
手动执行:
./gradlew :gsyVideoPlayer-compose:publishReleasePublicationToGsyvideoplayerRepository \
-PPUBLISH_TARGET=github
JitPack 默认会跑 ./gradlew install / publishToMavenLocal(详见根目录 jitpack.yml),compose 模块会随聚合发布产出,使用方式:
implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-compose:vX.Y.Z'
三个渠道都直接复用现有的 GitHub Action 与 jitpack.yml,无需额外为 compose 模块新增任务。本地烟雾测试推荐:
./gradlew :gsyVideoPlayer-compose:assembleRelease publishToMavenLocal
gsyVideoPlayer-compose/
└── src/main/java/com/shuyu/gsyvideoplayer/compose/
├── common/
│ └── LifecycleBridge.kt # 通用 Lifecycle ↔ Compose 桥
├── wrapper/
│ ├── GSYVideoPlayerView.kt # 模式一:包装 StandardGSYVideoPlayer
│ └── GSYAnyVideoPlayerView.kt # 模式一通用版:泛型 NormalGSYVideoPlayer
└── native_/
├── GSYComposeHostPlayer.java # 隐藏 UI 的 host player(仅留 surface_container)
├── GSYPlayerController.kt # 控制器 + State 同步
├── GSYPlayerState.kt # State / Snapshot 数据类
├── GSYPlayerSurface.kt # Surface + rememberGSYPlayerController
└── GSYDefaultControls.kt # 顶层 GSYComposePlayer + 默认控制条
下表帮助 0~1 接入业务在两种模式之间快速决策。列是常见诉求,行是模式:
| 诉求 | Wrapper(GSYVideoPlayerView / GSYAnyVideoPlayerView) | Native(GSYComposePlayer / GSYPlayerController + GSYPlayerSurface) |
|---|---|---|
| 几乎零成本接入、与 Java demo 一一对应 | ✅ 推荐 | ⚠️ 需要自己写 UI / 状态订阅 |
复用所有 GSYVideoOptionBuilder 链式选项(~30+ 项,含字幕、回调、缓存、滤镜) | ✅ 直接 .build(player) | ⚠️ 仅常用项有 controller 直挂入口(其余仍可通过 controller.setUp(builder) 完整透传,或 controller.withHost { player -> ... } 直访 host API) |
内置全屏(startWindowFullscreen)、SwitchUtil 无缝切换 | ✅ 完整 | ❌ 需要自己组合(见 D8 SwitchSeamless) |
| 列表多 item 各自独立播放(每行一个 player) | ✅ 但每个都是完整 View 树,性能略重 | ✅ 推荐:单 controller + LazyColumn 复用 surface |
| 多窗口"真并行"(同屏多路同时出声) | ⚠️ 受 GSYVideoManager 互斥 | ✅ D7 多 CustomManager 实例 |
| Compose 原生订阅 events / state(流式驱动 UI) | ❌ 内部仍是命令式 callback | ✅ 推荐:controller.events(SharedFlow) + controller.snapshot(Compose State<GSYPlayerSnapshot>,val s by controller.snapshot) / controller.stateFlow(StateFlow) |
| 完全自定义控件层(手势、按钮、字幕、画中画…) | ⚠️ 需要继承 StandardGSYVideoPlayer 重写 | ✅ 推荐:GSYPlayerSurface + 自绘 |
| 需要"播完接力下一段"且零黑屏 | ✅ Wrapper 路径走 SwitchUtil | ❌ 当前 Native setUp 会丢 surface 接管,会闪一下(P0-1 落地后改善,见 D7 注释) |
| 需要在不打断播放的前提下,跨 Activity / Fragment 复用同一个播放实例 | ⚠️ 需要手写 release 兜底 | ✅ controller rememberSaveable 化是后续 backlog |
简化决策:
下面 5 个常见场景给出"Java demo → Compose demo"的最小改造模板,仓库中均有对应 Activity 可直接对照。
Java:DetailPlayer 里 findViewById(R.id.detail_player) + GSYVideoOptionBuilder().setUrl(...).build(player)。
Compose:
GSYVideoPlayerView(
modifier = Modifier.fillMaxWidth().aspectRatio(16f / 9f),
setUp = { player ->
GSYVideoOptionBuilder()
.setUrl(url)
.setVideoTitle(title)
.setIsTouchWiget(true)
.build(player)
},
)
对照:BasicWrapperActivity.kt(同时演示 setVideoAllCallBack / setSeekRatio / setShowPauseCover / setReleaseWhenLossAudio 等 5 个高频 builder 选项)
Java:DetailPlayer 里 setRotateViewAuto(true).setLockLand(true).setNeedLockFull(true) + onBackPressed 兜底。
Compose(Wrapper):保持上方 builder 不变,把这三项打开即可,全屏由内部 OrientationUtils 接管;返回键拦截在 BackHandler { ... } 内调用 player.onBackFullscreen()。
Compose(Native):自绘控件层,监听 controller.snapshot.value.state.isPlaying + 自定义 fullscreenComposable 切换,参考 ListWithFullscreenActivity.kt。
Java:ListNormalAdapter 里手动管理"出屏 release / 入屏 setUp"。
Compose:单实例 controller + LazyColumn + snapshotFlow { firstVisible to lastVisible } 自动暂停。
val controller = rememberGSYPlayerController()
LaunchedEffect(listState, playingIndex) {
snapshotFlow { listState.firstVisibleItemIndex to (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1) }
.distinctUntilChanged()
.collect { (first, last) ->
if (playingIndex !in first..last) controller.pause()
}
}
对照:ListPlayNativeActivity.kt(含离屏 pause vs setUp 重置 两种语义切换 + 占位封面)
Java:builder.setSubTitle(srt).setVideoAllCallBack(new GSYSampleCallBack(){ ... })。
Compose(Wrapper):直接复用同一份 builder。
.setVideoAllCallBack(object : GSYSampleCallBack() {
override fun onPrepared(url: String?, vararg objects: Any?) {
super.onPrepared(url, *objects)
// ... 上报埋点 / 更新 UI state
}
})
Compose(Native):把 callback 替换为 controller.events(hot SharedFlow) / controller.stateFlow(StateFlow) / controller.snapshot(Compose State<GSYPlayerSnapshot>),用 LaunchedEffect { controller.events.collect { ... } } 即可订阅,完全不依赖 callback 接口。
| 场景 | 路径 | 说明 |
|---|---|---|
| 暴力切源(允许一次黑屏 + 重新 prepare) | controller.setUp(newUrl, ...) | SwitchUrlActivity.kt |
| 无缝切源(同 controller 跨 Composable 复用,零黑屏) | 同一 GSYPlayerController + GSYPlayerSurface 在 list/detail 间换位 attach(host 内部 setSurfaceToPlay 接管,等价 Java 的 SwitchUtil.savePlayState/clonePlayState) | SwitchSeamlessComposeActivity.kt |
| 列表自动接力下一段 | controller.events.collect { if (it is AutoComplete) controller.setUp(next) } | AutoPlayListActivity.kt (顶部 KDoc 注释了 surface 接管取舍) |
通用原则:Java 里"链式 builder + callback"的写法,Wrapper 路径几乎等价复用; Native 路径把 callback 换成
events/stateFlow/snapshot订阅,把 fullscreen 等粘性 View 行为改成"一个 Composable 状态切换"。