feat(video): add custom zoom controls

close #1159
This commit is contained in:
edde746
2026-05-29 00:06:47 +02:00
parent 47276e1bac
commit 2b7ea5fe02
52 changed files with 1123 additions and 106 deletions
@@ -122,6 +122,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
private var surfaceContainer: FrameLayout? = null
private var videoAspectContainer: AspectRatioFrameLayout? = null
private var subtitleView: SubtitleView? = null
private var videoZoomScale: Float = 1.0f
private var assHandler: AssHandler? = null
private var overlayLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
private var lastVideoSize: VideoSize? = null
@@ -1193,18 +1194,26 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
val containerHeight = contentView.height
if (containerWidth == 0 || containerHeight == 0) return
// In cover/stretch modes subtitles stay at container size so they never get
// cropped or distorted. In letterbox mode they follow the video rect so they
// anchor to the bottom of the video (matching MPV's default sub positioning).
// In cover/stretch/zoomed-in modes subtitles stay at container size so they
// never get cropped. In letterbox mode they follow the visible video rect.
val isLetterbox = videoAspectContainer?.resizeMode == AspectRatioFrameLayout.RESIZE_MODE_FIT
val (subWidth, subHeight) = if (isLetterbox) {
val videoAspect = (videoWidth * pixelRatio) / videoHeight
val containerAspect = containerWidth.toFloat() / containerHeight
if (videoAspect > containerAspect) {
val (baseWidth, baseHeight) = if (videoAspect > containerAspect) {
containerWidth to (containerWidth / videoAspect).toInt()
} else {
(containerHeight * videoAspect).toInt() to containerHeight
}
if (videoZoomScale < 0.999f) {
val zoomedWidth = ((baseWidth * videoZoomScale).toInt()).coerceAtLeast(1)
val zoomedHeight = ((baseHeight * videoZoomScale).toInt()).coerceAtLeast(1)
zoomedWidth to zoomedHeight
} else if (videoZoomScale > 1.001f) {
containerWidth to containerHeight
} else {
baseWidth to baseHeight
}
} else {
containerWidth to containerHeight
}
@@ -1235,6 +1244,21 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
}
}
fun setVideoZoom(scale: Double) {
if (disposing) return
val clamped = scale.coerceIn(0.5, 2.0).toFloat()
activity.runOnUiThread {
videoZoomScale = clamped
videoAspectContainer?.scaleX = clamped
videoAspectContainer?.scaleY = clamped
lastVideoSize?.let { vs ->
if (vs.width > 0 && vs.height > 0) {
updateSubtitleViewSize(vs.width, vs.height, vs.pixelWidthHeightRatio)
}
}
}
}
private fun emitTrackList() {
val player = exoPlayer ?: return
val tracks = player.currentTracks
@@ -148,6 +148,7 @@ class ExoPlayerPlugin :
}
"setSubtitleStyle" -> handleSetSubtitleStyle(call, result)
"setBoxFitMode" -> handleSetBoxFitMode(call, result)
"setVideoZoom" -> handleSetVideoZoom(call, result)
"setDvConversionMode" -> handleSetDvConversionMode(call, result)
"observeProperty" -> handleObserveProperty(call, result)
"setMpvProperty" -> handleSetMpvProperty(call, result)
@@ -550,6 +551,22 @@ class ExoPlayerPlugin :
} ?: result.success(null)
}
private fun handleSetVideoZoom(call: MethodCall, result: MethodChannel.Result) {
val scale = call.argument<Number>("scale")?.toDouble()
if (scale == null) {
result.error("INVALID_ARGS", "Missing 'scale'", null)
return
}
if (usingMpvFallback) {
result.success(null)
return
}
activity?.runOnUiThread {
playerCore?.setVideoZoom(scale)
result.success(null)
} ?: result.success(null)
}
private fun handleSetDvConversionMode(call: MethodCall, result: MethodChannel.Result) {
val mode = call.argument<String>("mode")
if (mode == null) {