Skip to content

Commit 16670e9

Browse files
committed
refactor(compose): P2 hardening for compose lib
P2-1 Narrow GSYComposeHostPlayer API surface - class and 3 constructors switched from public to package-private: only GSYPlayerController / GSYPlayerSurface inside com.shuyu.gsyvideoplayer.compose.native_ may use it. - Drop the redundant getCurrentStatePublic() bridge: the parent GSYVideoView already exposes public getCurrentState(). Controller now reads player.currentState directly. P2-2 Fix GSYPlayerSurface side-effect anti-pattern - rememberGSYPlayerController used remember(...) { setUp(); url } as its calculation lambda, mixing 'state computation' with a real I/O side-effect that may fire on each recomposition. - Replaced with LaunchedEffect(controller, url, ...) so it runs once per key change, on a Main-bound coroutine, with proper cancellation on dispose / key change. P2-3 Fix GSYDefaultControls slider seek storm - Old impl called controller.seekTo(...) on every onValueChange tick while dragging, which thrashed the player and caused stutter. - New impl keeps a local 'dragging: Float?' preview state: onValueChange only updates preview, onValueChangeFinished commits seekTo once. Time label and slider thumb both follow the preview while dragging, then snap back to the real position on release. P2-4 Drop dead config composeCompilerVersion - Removed `composeCompilerVersion = "1.5.14"` from gradle/dependencies.gradle. Since Kotlin 2.0+, compose-compiler is bundled with Kotlin and driven by the org.jetbrains.kotlin.plugin.compose Gradle plugin (already applied in gsyVideoPlayer-compose/build.gradle), so the property was unused and could mislead future maintainers. P2-5 Fix consumerProguardFiles dangling reference - gradle/lib.gradle declares consumerProguardFiles "consumer-rules.pro" but the file did not exist for the compose module, causing a silent skip of mergeReleaseConsumerProguardFiles and a 'Missing consumer ProGuard file' warning for downstream R8/Proguard users. - Added an empty (commented-only) consumer-rules.pro for the compose module. Verified mergeReleaseConsumerProguardFiles now runs. Verified locally: - GetDiagnostics: 0 issues - ./gradlew :gsyVideoPlayer-compose:assembleRelease :app:assembleDebug BUILD SUCCESSFUL - publishToMavenLocal default channel BUILD SUCCESSFUL -> ~/.m2/.../com/shuyu/gsyvideoplayer-compose/13.0.0/ - publishToMavenLocal -PPUBLISH_TARGET=mavenCentral BUILD SUCCESSFUL -> ~/.m2/.../io/github/carguo/gsyvideoplayer-compose/13.0.0/
1 parent a0ec158 commit 16670e9

6 files changed

Lines changed: 57 additions & 18 deletions

File tree

gradle/dependencies.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ ext {
66
permissionsDispatcherVersion = "4.9.1"
77
// Kotlin & Compose(仅 gsyVideoPlayer-compose 模块使用)
88
kotlinVersion = "2.0.21"
9-
composeCompilerVersion = "1.5.14"
9+
// 注意:Compose Compiler 版本由 `org.jetbrains.kotlin.plugin.compose` 跟随 kotlinVersion 自动管理,
10+
// 不再需要单独配置 composeCompilerVersion(Kotlin 2.0+ 之后 compose compiler 已并入 Kotlin 仓库)。
1011
composeBomVersion = "2024.06.00"
1112
activityComposeVersion = "1.9.0"
1213
lifecycleComposeVersion = "2.8.2"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Consumer ProGuard rules for gsyvideoplayer-compose.
2+
#
3+
# Compose 库自身不需要额外保留规则:
4+
# - androidx.compose.* 由 Google 官方 Compose 库随依赖自带 consumer rules;
5+
# - 本模块只是在 Compose 之上的薄包装,没有反射 / Native 调用 / Service / 序列化点。
6+
#
7+
# 该文件存在的目的,是满足上层 lib.gradle 中 `consumerProguardFiles "consumer-rules.pro"`
8+
# 的引用,避免下游依赖方在打开 R8/Proguard 时出现 "Missing consumer ProGuard file" 警告。

gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYComposeHostPlayer.java

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,23 @@
1212
* Compose 原生模式底层承载 Player:
1313
* 复用 StandardGSYVideoPlayer 的内核与渲染管线,但隐藏所有自带 UI,
1414
* 由 Compose 端自行绘制控制条与浮层。
15+
*
16+
* 包私有:仅在 com.shuyu.gsyvideoplayer.compose.native_ 内使用,
17+
* 通过 {@link GSYPlayerController} / {@link GSYPlayerSurface} 暴露给上层。
1518
*/
16-
public class GSYComposeHostPlayer extends StandardGSYVideoPlayer {
19+
class GSYComposeHostPlayer extends StandardGSYVideoPlayer {
1720

18-
public GSYComposeHostPlayer(Context context, Boolean fullFlag) {
21+
GSYComposeHostPlayer(Context context, Boolean fullFlag) {
1922
super(context, fullFlag);
2023
hideSelfWidgets();
2124
}
2225

23-
public GSYComposeHostPlayer(Context context) {
26+
GSYComposeHostPlayer(Context context) {
2427
super(context);
2528
hideSelfWidgets();
2629
}
2730

28-
public GSYComposeHostPlayer(Context context, AttributeSet attrs) {
31+
GSYComposeHostPlayer(Context context, AttributeSet attrs) {
2932
super(context, attrs);
3033
hideSelfWidgets();
3134
}
@@ -103,9 +106,4 @@ protected void touchSurfaceMoveFullLogic(float absDeltaX, float absDeltaY) {
103106

104107
@Override
105108
protected void touchDoubleUp(MotionEvent e) { /* 不响应双击 */ }
106-
107-
/** 暴露给 Compose 层使用 */
108-
public int getCurrentStatePublic() {
109-
return mCurrentState;
110-
}
111109
}

gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYDefaultControls.kt

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import androidx.compose.material3.Slider
1919
import androidx.compose.material3.Text
2020
import androidx.compose.runtime.Composable
2121
import androidx.compose.runtime.getValue
22+
import androidx.compose.runtime.mutableStateOf
23+
import androidx.compose.runtime.remember
24+
import androidx.compose.runtime.setValue
2225
import androidx.compose.ui.Alignment
2326
import androidx.compose.ui.Modifier
2427
import androidx.compose.ui.graphics.Color
@@ -59,6 +62,10 @@ fun GSYDefaultControls(
5962
) {
6063
val snap by controller.snapshot
6164

65+
// 拖拽中本地预览:dragging != null 时 UI 跟随手指,但不真正 seek;
66+
// 抬手(onValueChangeFinished)才一次性提交 seekTo,避免每帧 seek 卡顿。
67+
var dragging by remember { mutableStateOf<Float?>(null) }
68+
6269
Box(modifier) {
6370
// 中央按钮 / loading
6471
Box(Modifier.matchParentSize(), contentAlignment = Alignment.Center) {
@@ -107,15 +114,17 @@ fun GSYDefaultControls(
107114
tint = Color.White,
108115
)
109116
}
110-
Text(formatTime(snap.currentPosition), color = Color.White)
117+
Text(formatTime(displayPositionMs(snap.currentPosition, snap.duration, dragging)), color = Color.White)
111118
Box(modifier = Modifier.weight(1f).padding(horizontal = 8.dp)) {
112119
val durationMs: Long = snap.duration.coerceAtLeast(0L)
113-
val progress: Float = if (durationMs > 0L) {
120+
val livedProgress: Float = if (durationMs > 0L) {
114121
// 用 Double 计算,避免 Long 很大时 Float 精度丢失
115122
(snap.currentPosition.toDouble() / durationMs.toDouble())
116123
.toFloat()
117124
.coerceIn(0f, 1f)
118125
} else 0f
126+
// 拖拽中以本地预览覆盖,松开后回到真实播放进度
127+
val progress: Float = dragging ?: livedProgress
119128
val bufferProgress: Float = (snap.bufferPercent / 100f).coerceIn(0f, 1f)
120129

121130
// 缓冲进度条放在 Slider 之下作为背景,避免遮挡 Slider 的拖拽手势
@@ -131,8 +140,14 @@ fun GSYDefaultControls(
131140
Slider(
132141
value = progress,
133142
onValueChange = { v ->
134-
if (durationMs > 0L) {
135-
// Long * Double 防溢出;先 clamp 比例再换算
143+
// 拖拽期间:仅更新本地预览状态,不调用 seekTo
144+
dragging = v.coerceIn(0f, 1f)
145+
},
146+
onValueChangeFinished = {
147+
// 抬手:一次性提交 seek,并清除预览覆盖(让回写的真实进度接管)
148+
val v = dragging
149+
dragging = null
150+
if (v != null && durationMs > 0L) {
136151
val ratio = v.coerceIn(0f, 1f).toDouble()
137152
val target = (ratio * durationMs).toLong().coerceIn(0L, durationMs)
138153
controller.seekTo(target)
@@ -155,3 +170,14 @@ private fun formatTime(ms: Long): String {
155170
return if (h > 0) String.format(Locale.US, "%d:%02d:%02d", h, m, s)
156171
else String.format(Locale.US, "%02d:%02d", m, s)
157172
}
173+
174+
/**
175+
* 拖拽中:以本地预览比例 dragging * duration 显示;非拖拽:使用真实 currentPosition。
176+
*/
177+
private fun displayPositionMs(currentPosition: Long, duration: Long, dragging: Float?): Long {
178+
if (dragging == null) return currentPosition
179+
val safeDuration = duration.coerceAtLeast(0L)
180+
if (safeDuration <= 0L) return 0L
181+
val ratio = dragging.coerceIn(0f, 1f).toDouble()
182+
return (ratio * safeDuration).toLong().coerceIn(0L, safeDuration)
183+
}

gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerController.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ class GSYPlayerController internal constructor(
158158
}
159159

160160
private fun syncFromHost(player: GSYComposeHostPlayer) {
161-
val state = when (player.currentStatePublic) {
161+
val state = when (player.currentState) {
162162
GSYVideoView.CURRENT_STATE_NORMAL -> GSYPlayState.Idle
163163
GSYVideoView.CURRENT_STATE_PREPAREING -> GSYPlayState.Preparing
164164
GSYVideoView.CURRENT_STATE_PLAYING -> GSYPlayState.Playing
@@ -190,7 +190,7 @@ class GSYPlayerController internal constructor(
190190

191191
fun togglePlayPause() {
192192
val p = host ?: return
193-
when (p.currentStatePublic) {
193+
when (p.currentState) {
194194
GSYVideoView.CURRENT_STATE_PLAYING -> p.onVideoPause()
195195
GSYVideoView.CURRENT_STATE_PAUSE -> p.onVideoResume()
196196
GSYVideoView.CURRENT_STATE_AUTO_COMPLETE -> p.startPlayLogic()

gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerSurface.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.shuyu.gsyvideoplayer.compose.native_
22

33
import androidx.compose.runtime.Composable
44
import androidx.compose.runtime.DisposableEffect
5+
import androidx.compose.runtime.LaunchedEffect
56
import androidx.compose.runtime.remember
67
import androidx.compose.ui.Modifier
78
import androidx.compose.ui.viewinterop.AndroidView
@@ -24,9 +25,14 @@ fun rememberGSYPlayerController(
2425
val context = LocalContext.current
2526
val controller = remember { GSYPlayerController(context) }
2627
if (url != null) {
27-
remember(controller, url, cacheWithPlay, title, autoPlay) {
28+
// 关键约束:不能在 remember 的 calculation lambda 里执行 controller.setUp(...),
29+
// 那是把"副作用"放在了"读 state"的位置——重组期会被多次调用,
30+
// 也无法保证只在 key 变化时触发一次。改用 LaunchedEffect:
31+
// - key 变化时取消上一次协程并重新执行
32+
// - 离开 composition 时自动清理
33+
// - 在主线程的协程作用域内执行,避免 race
34+
LaunchedEffect(controller, url, cacheWithPlay, title, autoPlay) {
2835
controller.setUp(url, cacheWithPlay, title, autoPlay)
29-
url
3036
}
3137
}
3238
DisposableEffect(controller) {

0 commit comments

Comments
 (0)