mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 22:03:13 -06:00
Show the video in a rotated wall panel's screenshot, not a black rectangle
#236 gave each video-wall panel a mounting rotation, which for the first time puts a real rotation on an ancestor of the ExoPlayer TextureView. The screenshot compositor could not express that: it pasted the video frame with an axis-aligned Rect built from getLocationInWindow(), so on a rotated panel the frame landed outside the capture bitmap entirely. What reached the dashboard was the plain black that view.draw() leaves wherever a TextureView is — a panel that looks dead while it is playing perfectly, which is the worst thing a diagnostic can say. The frame is now placed through the same transform chain the hierarchy was drawn with, accumulated up the parent chain the way the framework does when it draws a child, so any ancestor rotation/translation is honoured. The bitmap is also scaled from the surface's own dimensions rather than assumed to match the view. Measured on the emulator, a wall panel playing video, remote screenshot vs the adb framebuffer at the same moment (standard deviation — 0 means a flat frame): before after rotation 0 sd 0.439 / truth 0.430 sd 0.443 / truth 0.435 (unchanged) rotation 90 sd 0 / truth 0.461 sd 0.448 / truth 0.448 rotation 90 sd 0 / truth 0.467 sd 0.460 / truth 0.457 rotation 90 sd 0 / truth 0.408 sd 0.463 / truth 0.466 Every rotated capture was #010101 with zero variance before; each now tracks the real framebuffer. Rotation 0 is unchanged, and so is the ordinary fullscreen (non-wall) path, re-measured across images and video. No unit test: this is android.graphics.Matrix semantics against a live view hierarchy, which the JVM test source set cannot exercise — the evidence is the before/after measurement above. Android 151/151, server 1298/1298. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
This commit is contained in:
parent
5d56e538af
commit
9a630087ff
|
|
@ -66,16 +66,25 @@ class ScreenshotCapture {
|
|||
if (tv.isAvailable && tv.visibility == View.VISIBLE) {
|
||||
val tvBitmap = tv.bitmap
|
||||
if (tvBitmap != null) {
|
||||
val loc = IntArray(2)
|
||||
tv.getLocationInWindow(loc)
|
||||
val rootLoc = IntArray(2)
|
||||
view.getLocationInWindow(rootLoc)
|
||||
val x = (loc[0] - rootLoc[0]).toFloat()
|
||||
val y = (loc[1] - rootLoc[1]).toFloat()
|
||||
val destRect = Rect(x.toInt(), y.toInt(), x.toInt() + tv.width, y.toInt() + tv.height)
|
||||
canvas.drawBitmap(tvBitmap, null, destRect, null)
|
||||
// Place the frame through the SAME transform chain the hierarchy was drawn
|
||||
// with, rather than an axis-aligned rect at getLocationInWindow().
|
||||
//
|
||||
// #236 gave a video-wall panel a mounting rotation, which puts a real
|
||||
// rotation on an ancestor of this TextureView. An axis-aligned rect cannot
|
||||
// express that, so the frame was pasted un-rotated at a position that fell
|
||||
// outside the capture bitmap entirely — and what the dashboard received was
|
||||
// the plain black that view.draw() leaves wherever a TextureView is, i.e. a
|
||||
// panel that looks dead while it is happily playing. Verified on the
|
||||
// emulator: at rotation 90 every remote screenshot of a video came back
|
||||
// #010101 with zero variance, while rotation 0 was correct.
|
||||
val m = matrixTo(tv, view)
|
||||
// The surface bitmap is not required to match the view's size.
|
||||
if (tvBitmap.width > 0 && tvBitmap.height > 0) {
|
||||
m.preScale(tv.width.toFloat() / tvBitmap.width, tv.height.toFloat() / tvBitmap.height)
|
||||
}
|
||||
canvas.drawBitmap(tvBitmap, m, null)
|
||||
tvBitmap.recycle()
|
||||
Log.d("ScreenshotCapture", "Composited TextureView at ($x,$y) size=${tv.width}x${tv.height}")
|
||||
Log.d("ScreenshotCapture", "Composited TextureView ${tv.width}x${tv.height} via $m")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -114,6 +123,28 @@ class ScreenshotCapture {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The matrix mapping [view]'s own coordinates into [ancestor]'s, by walking up the parent chain
|
||||
* and concatenating each step the way the framework does when it draws a child: the child's own
|
||||
* matrix (rotation/scale/translation about its pivot) followed by its layout offset.
|
||||
*
|
||||
* Stops at [ancestor], or at the top of the View chain if it is never reached — a partial chain
|
||||
* still places the frame better than ignoring the transform completely.
|
||||
*/
|
||||
private fun matrixTo(view: View, ancestor: View): android.graphics.Matrix {
|
||||
val out = android.graphics.Matrix()
|
||||
var v: View = view
|
||||
while (true) {
|
||||
val local = android.graphics.Matrix(v.matrix) // translationX/Y + rotation about pivot
|
||||
local.postTranslate(v.left.toFloat(), v.top.toFloat())
|
||||
out.postConcat(local) // out = local * out (child-first)
|
||||
val parent = v.parent
|
||||
if (parent !is View || parent === ancestor) break
|
||||
v = parent
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
private fun findAllTextureViews(view: View, result: MutableList<TextureView>) {
|
||||
if (view is TextureView) {
|
||||
result.add(view)
|
||||
|
|
|
|||
Loading…
Reference in a new issue