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:
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package pw.binom.viewmate.glasses
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import pw.binom.viewmate.glasses.ui.Rect
|
||||
import pw.binom.viewmate.glasses.ui.letterboxRect
|
||||
|
||||
class LetterboxRectTest {
|
||||
|
||||
@Test
|
||||
fun wideVideoIntoPortraitSurfaceLetterboxesTopBottom() {
|
||||
// Видео 16:9 (896×480) в поверхность 4:3 (640×480): поля сверху/снизу.
|
||||
val rect = letterboxRect(videoW = 896, videoH = 480, viewW = 640, viewH = 480)
|
||||
val expected = Rect(x0 = 0, y0 = 68, w = 640, h = 343)
|
||||
assertEquals(expected, rect)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun squareVideoIntoSideSurfaceLetterboxesSides() {
|
||||
// Видео 4:3 (640×480) в поверхность 16:9 (896×480): поля сбоку.
|
||||
val rect = letterboxRect(videoW = 640, videoH = 480, viewW = 896, viewH = 480)
|
||||
val expected = Rect(x0 = 128, y0 = 0, w = 640, h = 480)
|
||||
assertEquals(expected, rect)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sameAspectFillsSurface() {
|
||||
// Видео 16:9 (896×480) в поверхность 16:9 (896×480): полный вьюпорт.
|
||||
val rect = letterboxRect(videoW = 896, videoH = 480, viewW = 896, viewH = 480)
|
||||
val expected = Rect(x0 = 0, y0 = 0, w = 896, h = 480)
|
||||
assertEquals(expected, rect)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sameAspectFourThreeFillsSurface() {
|
||||
// Видео 4:3 (640×480) в поверхность 4:3 (640×480): полный вьюпорт.
|
||||
val rect = letterboxRect(videoW = 640, videoH = 480, viewW = 640, viewH = 480)
|
||||
val expected = Rect(x0 = 0, y0 = 0, w = 640, h = 480)
|
||||
assertEquals(expected, rect)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun zeroSizesReturnNull() {
|
||||
assertNull(letterboxRect(0, 0, 640, 480), "видео неизвестно — полный вьюпорт")
|
||||
assertNull(letterboxRect(896, 480, 0, 0), "поверхность не готова — полный вьюпорт")
|
||||
assertNull(letterboxRect(896, 0, 640, 480), "высота видео неизвестна — полный вьюпорт")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user