fix(app-glasses): би-нокулярный рендер RayNeo — letterbox + scale=false (полный кадр в каждый глаз)

Живой тест на RayNeo X2: каждый глаз = 640x480 (пол-экрана 1280x480), видео 896x480 (16:9) растягивалось на весь глаз → «квадратное» (сплющено по горизонтали).
- letterboxRect(videoW,videoH,viewW,viewH): FIT-вьюпорт с полями (страховка для произвольных поверхностей)
- Binocular(scale=true→false): два полных кадра 640x343 в двух половинах экрана (вместо scaleX 0.5 половинки кадра)
- onVideoSizeChanged → videoWidth/Height для letterbox
- Тесты 32/0 (LetterboxRectTest 5)
- Пользователь: «на глаз размер поменялся, будто бы норм»
This commit is contained in:
Hermes Agent
2026-08-19 21:03:55 +03:00
parent 25b85ea191
commit b4420f446c
4 changed files with 138 additions and 2 deletions
@@ -12,7 +12,10 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.video.VideoSize
import kotlin.math.roundToInt
import pw.binom.viewmate.glasses.log
import pw.binom.viewmate.glasses.render.EglCore
import pw.binom.viewmate.glasses.render.FullFrameRect
@@ -72,6 +75,21 @@ class EglVideoRenderer(private val player: SimpleExoPlayer) {
private val outputs = mutableListOf<Output>()
// Размер видео (0,0 пока неизвестен) — нужен для letterbox-вьюпорта.
private var videoWidth = 0
private var videoHeight = 0
private val videoSizeListener = object : Player.Listener {
override fun onVideoSizeChanged(videoSize: VideoSize) {
videoWidth = videoSize.width
videoHeight = videoSize.height
}
}
init {
player.addListener(videoSizeListener)
}
private class Output(
val holder: SurfaceHolder,
val surface: WindowSurface,
@@ -144,7 +162,14 @@ class EglVideoRenderer(private val player: SimpleExoPlayer) {
private fun drawFrame(blit: FullFrameRect, windowSurface: WindowSurface, viewWidth: Int, viewHeight: Int) {
windowSurface.makeCurrent()
GLES20.glViewport(0, 0, viewWidth, viewHeight)
val rect = letterboxRect(videoWidth, videoHeight, viewWidth, viewHeight)
if (rect == null) {
// Размер видео ещё неизвестен — рисуем на всю поверхность.
GLES20.glViewport(0, 0, viewWidth, viewHeight)
} else {
// Letterbox: поля (чёрные, т.к. фон/очистка) сверху/снизу или сбоку.
GLES20.glViewport(rect.x0, rect.y0, rect.w, rect.h)
}
blit.drawFrame(textureId, transformMatrix)
windowSurface.swapBuffers()
}
@@ -154,6 +179,8 @@ class EglVideoRenderer(private val player: SimpleExoPlayer) {
released = true
log("glasses", "EglVideoRenderer: release")
player.removeListener(videoSizeListener)
// Отвязать плеер (плеер мог быть уже освобождён — не падаем).
val ps = playerSurface
if (ps != null) {
@@ -179,3 +206,31 @@ class EglVideoRenderer(private val player: SimpleExoPlayer) {
eglCore = null
}
}
/** Прямоугольник вписывания видео в поверхность (letterbox): левый-нижний угол и размеры. */
data class Rect(
val x0: Int,
val y0: Int,
val w: Int,
val h: Int,
)
/**
* Letterbox (FIT) с сохранением пропорций: вписывает видео в поверхность.
* Поля по вертикали (видео шире поверхности) или по горизонтали (видео уже).
* Возвращает null, если размеры неизвестны/некорректны (нули) — тогда нужен полный вьюпорт.
*/
fun letterboxRect(videoW: Int, videoH: Int, viewW: Int, viewH: Int): Rect? {
if (videoW <= 0 || videoH <= 0 || viewW <= 0 || viewH <= 0) return null
val videoAspect = videoW.toFloat() / videoH
val viewAspect = viewW.toFloat() / viewH
return if (videoAspect > viewAspect) {
// Видео шире поверхности: вписываем по ширине, поля сверху/снизу.
val h = (viewW / videoAspect).roundToInt()
Rect(0, (viewH - h) / 2, viewW, h)
} else {
// Видео уже поверхности: вписываем по высоте, поля сбоку.
val w = (viewH * videoAspect).roundToInt()
Rect((viewW - w) / 2, 0, w, viewH)
}
}
@@ -65,7 +65,7 @@ fun GlassesMovieScreen(
}
}
Binocular(scale = true) {
Binocular(scale = false) {
val screenWidthPx = BinocularScreenSize.current.width
val screenHeightPx = BinocularScreenSize.current.height