fix(android): paint the letterbox bars, so stale pixels stop surviving in them
Native video left debris in the padding around the video: the "previous frame" flash on rotation, a ghost copy of the control bar stranded in the top bar, each new clock digit drawn over the one before it (35:42 with the 1 still showing through the 2), and the sleep/quality menus leaving their imprint after closing. One cause under all of it — nothing painted those bars. The window surface is opaque; the theme is not translucent and dumpsys window shows no translucency flag. For an opaque surface HWUI deliberately does NOT clear the damaged region before replaying a frame: it assumes the view hierarchy covers every pixel it owns. Here that hierarchy is window background → video TextureView → transparent WebView, and fitSurfaceToScreen sizes the TextureView to the letterboxed video rect. So the bars were the window background's alone to paint, and setTransparent(true) cleared it to TRANSPARENT — leaving them painted by nobody, with whatever was last in the framebuffer surviving there. The window background now stays opaque black while compositing. It cannot hide the video: the TextureView is drawn on top of it, and the WebView's own background is what lets the picture through. Three previous attempts missed because they aimed at the window's rotation animation and at TextureView frame-retention — two postOnAnimation hops, an onSurfaceTextureUpdated reveal, then ROTATION_ANIMATION_JUMPCUT with FLAG_FULLSCREEN to make it stick. The pixels were never the animation's, which is also why the artefact reproduces standing still, with no rotation involved. Those are removed. The alpha-hiding among them actively made things worse: it blanked the one view that reliably paints its own rect. FLAG_FULLSCREEN goes too — it fought edge-to-edge insets for no gain. Verified on device (HONOR ROD2-W09, Android 16): reproduced with native video on — ghost control bar in the top bar, doubled clock digit — then absent after the fix across playback, the control bar and a rotation round-trip. DR-194 is rewritten to record the real mechanism and marked Done.
This commit is contained in:
@@ -349,7 +349,7 @@ Internal architecture, components, and application logic.
|
||||
| DR-188 | Native Android video is **ready to be the default except for the background-audio handoff**, and the flip therefore waits. The picture defects behind DR-172 are all found, fixed and device-verified — DR-185 (the app shell painted over the surface through a CSS rule targeting an attribute nothing set), DR-182 (nothing could lift the poster card on a path with no `<video>` element), DR-183 (the JS bridges raced the page load, so `setTransparent(true)` could never arrive), DR-184 (the SurfaceView was never detached), plus DR-186 and DR-187, the two UI defects only this path could reveal. On a device logcat now carries `WebView transparent = true` and `Marking media ready` with video on screen, which is the pair DR-172 went looking for and could not find, and skip, seek and rotation were exercised by hand. Turning the default on then surfaced a *different* unverified sub-path: returning from background audio is HTML5-only (DR-190), so on the native path playback simply stays dead. Shipping it would have repeated DR-161 exactly — a verified sub-path made default over an unverified one — so the default stays off and the flip is gated on DR-190 rather than on more confidence | UI | UR-003, UR-004, UR-041 | Blocked by DR-190 |
|
||||
| DR-191 | Forcing the WebView overlay to redraw from the Activity, because with the ExoPlayer **SurfaceView** beneath it the overlay's ordinary damage stopped reaching the screen: the page kept mutating — the clock text every second, the control bar's opacity going to 0 — while the display held whatever frame it last presented, over video that animated perfectly. Not a state defect; the live DOM showed the slider advancing 476 → 479 across three seconds behind a screen showing neither. Only **structural** changes got through, which is why the play overlay always appeared to work (an `{#if}` block, added and removed) while the progress bar never did, and why rotation lost the transport UI. A CSS animation cannot help, since opacity animates on the compositor without repainting the layer. **Superseded by DR-192**: this drove `postInvalidateOnAnimation` in a loop, which treats the symptom — the cause is the SurfaceView's separate layer, and removing that removes the need. Kept as the record of how the mechanism was identified | Android | UR-003, UR-004 | Superseded by DR-192 |
|
||||
| DR-195 | Play/pause works on the native path, because the frontend stops claiming a webview element is playing when there is none. `html5_playing` is Rust's record of "a webview `<video>` is active and in this state", and `toggle_playback`, `play` and `pause` all route transport to that element whenever it is set. The player route mirrored element state into it **unconditionally** — from `handleReportStart` and, fatally, from `handleReportProgress`, which VideoPlayer calls on a 10-second interval — so on the native path the frontend re-declared every ten seconds that an element was playing when none existed, and every transport intent was emitted into the void. The pause button was dead from the on-screen tap, from the control bar, and from a direct `player_toggle` invocation, while seek and skip kept working because `player_seek_video` decides elsewhere; that asymmetry is the signature. It also explains the flashing, since the control bar and the JRay overlay both key off `isPlaying`, which was being contradicted on every interval tick. DR-193 clearing the flag at load was necessary but insufficient on its own — the interval put it straight back. The mirror now lives in `mirrorElementStateToRust` in VideoPlayer, gated on `useHtml5Element`, which is the only place that knows whether an element renders at all; the route cannot tell the two paths apart, which is precisely how it came to lie. Confirmed on device by ADB: surface tap and control bar each pause (position frozen across repeated samples, transport label flipped) and resume | Playback | UR-005, UR-003 | Done |
|
||||
| DR-194 | The previous frame flashing on rotation. It reads as a TextureView artefact — the view retains its last frame, so between a rotation and `fitSurfaceToScreen()` landing that frame sits at the old size — and two fixes were built on that reading: revealing after two `postOnAnimation` hops, then revealing on `onSurfaceTextureUpdated`, which required owning the `SurfaceTextureListener` and handing ExoPlayer the Surface directly rather than via `setVideoTextureView`. **Neither stopped the flash.** The mechanism is the *window's* rotation animation: Android cross-fades a **screenshot of the old orientation**, that screenshot contains the old video frame at the old size, and no TextureView bookkeeping can reach it — nor can the app pre-empt the screenshot, since `onConfigurationChanged` fires after it is taken. The only lever is to stop the animation: `ROTATION_ANIMATION_JUMPCUT`. That was accepted and silently ignored at first, and the platform said why out loud — `VRI[MainActivity]: setLayoutParams: not fullscreen` — because the attribute is honoured only for a fullscreen window. `FLAG_FULLSCREEN` (deprecated for hiding system bars, which immersive mode does instead, but still what marks the window fullscreen for this decision) is therefore set alongside it, scoped to while native compositing is active so the rest of the app keeps its normal animation. The frame-arrival reveal is kept: it replaced a fixed-timeout guess with a real signal, and its timeout is required rather than defensive, since a resize while paused means no new frame is ever coming. **The flash is not confirmed fixed on device** — the forced-rotation harness (`settings put system user_rotation`) proved unreliable, and `screenrecord` fixes its canvas at start so a rotation inside a recording never changes frame dimensions, which defeated two attempts at measuring it | Android | UR-003, UR-066 | Needs device verification |
|
||||
| DR-194 | Stale pixels in the letterbox bars — the rotation "flash of the previous frame", a ghost control bar stranded in the top bar, each new clock digit drawn over the last (`35:42` with the `1` still showing through the `2`), and menus (sleep timer, quality) leaving their imprint behind. One cause for all of it: **nothing painted the bars.** The window surface is opaque (the theme is not translucent), and for an opaque surface HWUI deliberately does not clear the damaged region before replaying a frame — it assumes the view hierarchy covers every pixel. That hierarchy is window background → video `TextureView` → transparent WebView, and `fitSurfaceToScreen` sizes the TextureView to the *letterboxed* video rect, so the bars were the window background's alone to paint. `setTransparent(true)` cleared that background to `TRANSPARENT`, leaving the bars painted by nobody and whatever was last in the framebuffer surviving in them. Fixed by keeping the window background opaque black while compositing; the WebView's own background is what lets the video through, and the TextureView is drawn on top of the window background, so an opaque one cannot hide it. Three earlier fixes aimed at the window's rotation animation and at TextureView frame-retention (two `postOnAnimation` hops, an `onSurfaceTextureUpdated` reveal, then `ROTATION_ANIMATION_JUMPCUT` + `FLAG_FULLSCREEN`) all missed, because the pixels were never the animation's; the alpha-hiding among them made it worse by blanking the one view that reliably paints its own rect. Those are removed, `FLAG_FULLSCREEN` included — it fought edge-to-edge insets for no gain. Verified on device: ghosting reproduced with native video on, then absent after the fix, across playback, the control bar and a rotation round-trip | Android | UR-003, UR-066 | Done |
|
||||
| DR-193 | Play/pause reaches the player that is actually rendering. `toggle_playback`, `play` and `pause` all route to the webview element when `is_html5_active()`, which is `html5_playing.is_some()` — a flag written **only** by the element's own state reports and cleared only when it reports "stopped"/"idle" (or on a background-audio handoff). An element that went away without that final report, or webview-rendered music earlier in the same process, therefore left the flag set, and on Android's native video path every transport intent was emitted as a `ControlCommand` at an element that no longer existed: the pause button did nothing, from the on-screen tap and from the control bar alike, while seek and skip kept working because `player_seek_video` decides elsewhere. Whether it happened at all depended on what had played before, which is exactly what made it read as flaky rather than broken. `load_and_play` — the native load path, and the one the HTML5 video path deliberately avoids via `set_current_item` — now clears the flag, because loading into the native backend *is* the statement that native renders this item. Nothing is lost on the webview path: an element re-establishes its own authority the moment it reports again, so this is the existing "element is gone" semantics applied where it can be known directly rather than inferred from a report that may never arrive | Playback | UR-005, UR-003 | Done |
|
||||
| DR-192 | Native video presents through a **TextureView**, not a SurfaceView. A SurfaceView renders on its own layer *outside* the app window and punches a transparent region through it; everything drawn above that hole — for us the entire Svelte UI in a transparent WebView — depends on that composition path, and Android's own graphics documentation states that "overlays do not currently work correctly with SurfaceView or TextureView". The consequences were four symptoms of one cause (DR-191): a frozen progress bar, controls that would not fade, rotation losing the transport UI, and overlays that lingered after the DOM removed them. A TextureView is an ordinary view whose frames are drawn as a texture in the window's normal rendering pass, so there is no second layer and no transparent region, and the WebView above composites like it would over any other view — which is why media3 offers `surface_type="texture_view"` and why it is the standard remedy for ExoPlayer overlay problems. The trade is accepted rather than hidden: TextureView costs more power and memory than SurfaceView and adds a frame of latency, but hardware decode through MediaCodec is untouched, so the reason native video exists survives it. `setVideoTextureView` installs ExoPlayer's own `SurfaceTextureListener`, so the old `SurfaceHolder.Callback` wiring is deleted rather than ported — adding a listener of ours would displace it and the video would never appear. PiP needs no change, since a TextureView is a View and the aspect-ratio probe reads its measured bounds | Android | UR-003, UR-004, UR-041 | Done |
|
||||
| DR-190 | The background-audio handoff can return to the native path. Everything that restores playback on the way back is written around the WebView `<video>`: `applyPendingForegroundSeek` returns early on `!videoElement`, the HLS re-init `$effect` returns early on `!useHtml5Element`, and `pendingForegroundSeek`/`pendingForegroundPlay` — which own the post-handoff position and play/pause — are consumed only by `handleCanPlay` and `markMediaReady`, an element event and a path that reaches the same guard. On the native path there is no element, so `exitBackgroundAudioHandoff` completes, clears `handoffState`, blanks and reassigns `currentStreamUrl` to force an effect that will not run, and nothing ever restarts ExoPlayer: the user returns from the lockscreen to a dead player. This never showed while the path was opt-in and its picture was invisible anyway. The return needs the native equivalent of the element reload — re-issue the item to the backend, seek to the position `player_exit_background_audio` reports, then honour `wasPlaying` — routed through the adapter rather than the element, so both paths restore through one contract | Playback | UR-040, UR-003 | Proposed |
|
||||
|
||||
+4449
-4449
File diff suppressed because it is too large
Load Diff
@@ -191,37 +191,6 @@ class MainActivity : TauriActivity() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotation (and any other config change this Activity handles itself).
|
||||
*
|
||||
* Two things have to happen here rather than later, and both are about the
|
||||
* *previous* video frame surviving the transition:
|
||||
*
|
||||
* - The video view is hidden until a new frame arrives. The equivalent call
|
||||
* in `fitSurfaceToScreen` runs from the content view's layout listener,
|
||||
* which is after the rotation — by then the stale frame has been on screen
|
||||
* for the whole transition.
|
||||
* - The window's rotation animation is a **cross-fade of a screenshot** of
|
||||
* the old orientation, and that screenshot contains the old video frame at
|
||||
* the old size. No amount of TextureView bookkeeping can touch it, which is
|
||||
* why hiding on frame-arrival alone did not stop the flash. `JUMPCUT` drops
|
||||
* the cross-fade, so there is no old frame to fade through; it is set only
|
||||
* while native compositing is active (see setTransparent) so the rest of
|
||||
* the app keeps the normal animation.
|
||||
*
|
||||
* TRACES: UR-003, UR-066 | DR-194
|
||||
*/
|
||||
override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
try {
|
||||
if (com.dtourolle.jellytau.player.JellyTauPlayer.isInitialized()) {
|
||||
com.dtourolle.jellytau.player.JellyTauPlayer.getInstance().hideUntilFreshFrame()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.w("MainActivity", "hideUntilFreshFrame on config change failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(
|
||||
isInPictureInPictureMode: Boolean,
|
||||
newConfig: android.content.res.Configuration
|
||||
@@ -401,45 +370,46 @@ class MainActivity : TauriActivity() {
|
||||
@JavascriptInterface
|
||||
fun setTransparent(transparent: Boolean) {
|
||||
handler.post {
|
||||
val color = if (transparent) {
|
||||
mediaWebView?.setBackgroundColor(
|
||||
if (transparent) {
|
||||
android.graphics.Color.TRANSPARENT
|
||||
} else {
|
||||
android.graphics.Color.BLACK
|
||||
}
|
||||
mediaWebView?.setBackgroundColor(color)
|
||||
// The WebView's window/surface must also stop painting opaque, or a
|
||||
// hardware-accelerated WebView still composites its own background.
|
||||
window.setBackgroundDrawable(
|
||||
android.graphics.drawable.ColorDrawable(color)
|
||||
)
|
||||
// Drop the rotation cross-fade while a native video surface is
|
||||
// composited behind the page. The animation fades a *screenshot* of
|
||||
// the old orientation, which still holds the previous video frame at
|
||||
// the old size — that is the "previous frame flashing in the black
|
||||
// bars", and it lives in the window animation rather than in
|
||||
// anything the TextureView owns. (DR-194)
|
||||
val attrs = window.attributes
|
||||
attrs.rotationAnimation = if (transparent) {
|
||||
android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT
|
||||
} else {
|
||||
android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE
|
||||
}
|
||||
window.attributes = attrs
|
||||
// `rotationAnimation` is honoured only for a **fullscreen** window —
|
||||
// the platform says so out loud, logging
|
||||
// "VRI[MainActivity]: setLayoutParams: not fullscreen" when the
|
||||
// attribute is set on ours, and then animating normally regardless.
|
||||
// Without this the JUMPCUT above is accepted and ignored, and the
|
||||
// cross-fade keeps showing the old orientation's screenshot, stale
|
||||
// video frame and all. FLAG_FULLSCREEN is deprecated for *hiding
|
||||
// system bars* (immersive mode does that, on player entry), but it
|
||||
// is still what marks the window fullscreen for this decision.
|
||||
@Suppress("DEPRECATION")
|
||||
if (transparent) {
|
||||
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
} else {
|
||||
window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
}
|
||||
// The WINDOW background stays OPAQUE — including while compositing.
|
||||
// It is the only thing that paints the pixels the video does not
|
||||
// cover, and clearing it was the whole defect.
|
||||
//
|
||||
// This window's surface is opaque: the theme is not translucent and
|
||||
// `dumpsys window` shows no translucency flag on it. For an opaque
|
||||
// surface HWUI deliberately does NOT clear the damaged region before
|
||||
// replaying a frame — it assumes the view hierarchy paints every
|
||||
// pixel it owns. That hierarchy is: window background, then the video
|
||||
// TextureView, then this transparent WebView. `fitSurfaceToScreen`
|
||||
// sizes the TextureView to the *letterboxed* video rect, so the bars
|
||||
// around the video are painted by the window background and nothing
|
||||
// else.
|
||||
//
|
||||
// Setting that background TRANSPARENT therefore left the bars painted
|
||||
// by nobody, and stale framebuffer content simply survived in them:
|
||||
// a whole ghost copy of the control bar stranded in the top bar, and
|
||||
// each new clock digit composited over the one before it ("35:42"
|
||||
// with the 1 still showing through the 2). The rotation flash is the
|
||||
// same bug at full-screen scale — the pre-rotation image persisting
|
||||
// in what became the new bars — which is why neither
|
||||
// ROTATION_ANIMATION_JUMPCUT nor revealing on frame arrival ever
|
||||
// touched it. Both were aimed at the window animation; the pixels
|
||||
// were never the animation's.
|
||||
//
|
||||
// The WebView's own background, set above, is what lets the video
|
||||
// through. An opaque window background cannot hide it: the
|
||||
// TextureView is drawn on top of it, not under it.
|
||||
//
|
||||
// TRACES: UR-003, UR-066 | DR-194
|
||||
window.setBackgroundDrawable(
|
||||
android.graphics.drawable.ColorDrawable(android.graphics.Color.BLACK)
|
||||
)
|
||||
android.util.Log.d("MainActivity", "WebView transparent = $transparent")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,12 +38,6 @@ class JellyTauPlayer(private val appContext: Context) {
|
||||
/** AudioEffect priority. Positive = higher priority than the default. */
|
||||
private const val EFFECT_PRIORITY = 1000
|
||||
|
||||
/**
|
||||
* How long to wait for a fresh frame after a resize before revealing the
|
||||
* view anyway. Playback may be paused, in which case no frame is coming.
|
||||
*/
|
||||
private const val FRESH_FRAME_TIMEOUT_MS = 400L
|
||||
|
||||
/**
|
||||
* Canonical 10-band ISO centre frequencies (Hz), mirroring EQ_BANDS in
|
||||
* settings.rs. Kept in sync deliberately: Rust owns the band layout, this
|
||||
@@ -236,12 +230,6 @@ class JellyTauPlayer(private val appContext: Context) {
|
||||
/** The Surface handed to ExoPlayer, owned here rather than by the player. */
|
||||
private var videoSurface: android.view.Surface? = null
|
||||
|
||||
/**
|
||||
* True while the view is hidden waiting for a new frame after a resize.
|
||||
* See fitSurfaceToScreen (DR-194).
|
||||
*/
|
||||
@Volatile
|
||||
private var awaitingFreshFrame = false
|
||||
/** Last reported video frame size, used to fit the surface to the screen preserving aspect ratio */
|
||||
private var videoWidth: Int = 0
|
||||
private var videoHeight: Int = 0
|
||||
@@ -1141,12 +1129,15 @@ class JellyTauPlayer(private val appContext: Context) {
|
||||
isOpaque = true
|
||||
|
||||
// Own the listener rather than calling `setVideoTextureView`,
|
||||
// which installs ExoPlayer's own and leaves us blind to frame
|
||||
// arrival. `onSurfaceTextureUpdated` is the only honest signal
|
||||
// that a NEW frame has landed in the texture, and that is
|
||||
// precisely what the letterbox artefact waits on — see
|
||||
// fitSurfaceToScreen. Handing ExoPlayer the Surface directly is
|
||||
// the same wiring `setVideoTextureView` does internally.
|
||||
// which installs ExoPlayer's own. Handing ExoPlayer the Surface
|
||||
// directly is the same wiring `setVideoTextureView` does
|
||||
// internally, and owning the listener keeps surface creation and
|
||||
// teardown symmetrical with `videoSurface` below.
|
||||
//
|
||||
// (This was originally introduced to observe frame arrival for
|
||||
// the letterbox artefact. That turned out to be the wrong lead —
|
||||
// see fitSurfaceToScreen — but the explicit wiring is worth
|
||||
// keeping on its own terms.)
|
||||
//
|
||||
// TRACES: UR-003, UR-004 | DR-194
|
||||
surfaceTextureListener = object : TextureView.SurfaceTextureListener {
|
||||
@@ -1180,12 +1171,6 @@ class JellyTauPlayer(private val appContext: Context) {
|
||||
override fun onSurfaceTextureUpdated(
|
||||
texture: android.graphics.SurfaceTexture
|
||||
) {
|
||||
// A genuinely new frame is now in the texture, so
|
||||
// whatever was retained from before the resize is gone.
|
||||
if (awaitingFreshFrame) {
|
||||
awaitingFreshFrame = false
|
||||
videoView?.alpha = 1f
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1258,31 +1243,6 @@ class JellyTauPlayer(private val appContext: Context) {
|
||||
* pillarbox). A raw SurfaceView with MATCH_PARENT otherwise stretches the
|
||||
* video to the surface bounds, which crops the bottom on rotation.
|
||||
*/
|
||||
/**
|
||||
* Hide the video view now, and keep it hidden until a genuinely new frame
|
||||
* arrives (or the timeout fires).
|
||||
*
|
||||
* Called from `MainActivity.onConfigurationChanged`, i.e. at the *start* of a
|
||||
* rotation. [fitSurfaceToScreen] is too late for this: it runs from the
|
||||
* content view's layout listener, after the rotation has already happened,
|
||||
* so the stale frame has been on screen for the whole transition by then.
|
||||
*
|
||||
* TRACES: UR-003, UR-066 | DR-194
|
||||
*/
|
||||
fun hideUntilFreshFrame() {
|
||||
mainHandler.post {
|
||||
val view = videoView ?: return@post
|
||||
awaitingFreshFrame = true
|
||||
view.alpha = 0f
|
||||
mainHandler.postDelayed({
|
||||
if (awaitingFreshFrame) {
|
||||
awaitingFreshFrame = false
|
||||
videoView?.alpha = 1f
|
||||
}
|
||||
}, FRESH_FRAME_TIMEOUT_MS)
|
||||
}
|
||||
}
|
||||
|
||||
fun fitSurfaceToScreen() {
|
||||
mainHandler.post {
|
||||
val view = videoView ?: return@post
|
||||
@@ -1318,43 +1278,19 @@ class JellyTauPlayer(private val appContext: Context) {
|
||||
lp.gravity = android.view.Gravity.CENTER
|
||||
}
|
||||
|
||||
// Hide the view across a resize, and reveal it when a genuinely NEW
|
||||
// video frame lands in the texture.
|
||||
// Deliberately no alpha-hiding across the resize.
|
||||
//
|
||||
// A TextureView retains its last frame. Between a rotation and this
|
||||
// re-fit landing, that retained frame is stretched across the OLD
|
||||
// rect — larger than the new one along at least one axis — so the
|
||||
// previous frame flashes in what should be the letterbox bars.
|
||||
//
|
||||
// Waiting a fixed number of animation frames does NOT fix it, which
|
||||
// the first attempt at this proved on device: an animation frame is
|
||||
// not a video frame, and at 24fps the next decoded frame can be
|
||||
// several vsyncs away. The tell was that pausing and playing cleared
|
||||
// the artefact by hand — that forces a fresh frame, which is the
|
||||
// real precondition. So the reveal is driven by
|
||||
// `onSurfaceTextureUpdated` instead.
|
||||
//
|
||||
// The timeout is not belt-and-braces, it is required: if playback is
|
||||
// paused when the resize happens, no new frame is coming and the
|
||||
// video would stay invisible forever. Revealing a stale frame after
|
||||
// a beat is strictly better than a permanently black player.
|
||||
//
|
||||
// Scoped to an actual size change so steady-state playback never
|
||||
// touches alpha.
|
||||
// Two earlier attempts hid the view here (and from
|
||||
// onConfigurationChanged) until a fresh frame landed, on the reading
|
||||
// that the letterbox flash was a retained TextureView frame drawn at
|
||||
// the old size. It was not: the bars were showing stale *framebuffer*
|
||||
// content because nothing painted them — see the window-background
|
||||
// note in MainActivity.setTransparent. Hiding the video view made
|
||||
// that strictly worse, since the TextureView is the one view in the
|
||||
// hierarchy that reliably paints its own rect; dropping its alpha to
|
||||
// 0 simply widened the un-painted area.
|
||||
//
|
||||
// TRACES: UR-003, UR-066 | DR-194
|
||||
val sizeChanged = lp.width != targetW || lp.height != targetH
|
||||
if (sizeChanged) {
|
||||
awaitingFreshFrame = true
|
||||
view.alpha = 0f
|
||||
mainHandler.postDelayed({
|
||||
if (awaitingFreshFrame) {
|
||||
awaitingFreshFrame = false
|
||||
videoView?.alpha = 1f
|
||||
}
|
||||
}, FRESH_FRAME_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
lp.width = targetW
|
||||
lp.height = targetH
|
||||
view.layoutParams = lp
|
||||
|
||||
Reference in New Issue
Block a user