feat(android): flutter surfaceview rendering
This commit is contained in:
@@ -1,12 +1,9 @@
|
||||
package com.edde746.plezy
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.app.AppOpsManager
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.Context
|
||||
@@ -14,14 +11,12 @@ import android.content.res.Configuration
|
||||
import android.util.Log
|
||||
import android.util.Rational
|
||||
import android.view.KeyEvent
|
||||
import android.view.TextureView
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.content.FileProvider
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterShellArgs
|
||||
import io.flutter.embedding.android.FlutterTextureView
|
||||
import io.flutter.embedding.android.RenderMode
|
||||
import io.flutter.embedding.android.TransparencyMode
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
@@ -149,51 +144,13 @@ class MainActivity : FlutterActivity() {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getRenderMode(): RenderMode {
|
||||
// Use TextureView so Flutter doesn't occupy a SurfaceView layer.
|
||||
// This allows the libass subtitle SurfaceView to sit between video and Flutter UI.
|
||||
return RenderMode.texture
|
||||
}
|
||||
override fun getRenderMode(): RenderMode = RenderMode.surface
|
||||
|
||||
override fun getTransparencyMode(): TransparencyMode {
|
||||
// Keep Flutter transparent so video/subtitles are visible below.
|
||||
return TransparencyMode.transparent
|
||||
}
|
||||
|
||||
override fun onFlutterTextureViewCreated(flutterTextureView: FlutterTextureView) {
|
||||
val original = flutterTextureView.surfaceTextureListener ?: return
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
var pendingResize: Runnable? = null
|
||||
var lastWidth = 0
|
||||
var lastHeight = 0
|
||||
|
||||
flutterTextureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
|
||||
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, w: Int, h: Int) {
|
||||
original.onSurfaceTextureAvailable(surface, w, h)
|
||||
}
|
||||
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, w: Int, h: Int) {
|
||||
if (w == lastWidth && h == lastHeight) return
|
||||
lastWidth = w; lastHeight = h
|
||||
pendingResize?.let { handler.removeCallbacks(it) }
|
||||
pendingResize = Runnable {
|
||||
if (flutterTextureView.isAvailable) {
|
||||
original.onSurfaceTextureSizeChanged(surface, w, h)
|
||||
}
|
||||
}
|
||||
handler.postDelayed(pendingResize!!, 100)
|
||||
}
|
||||
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
|
||||
original.onSurfaceTextureUpdated(surface)
|
||||
}
|
||||
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
|
||||
pendingResize?.let { handler.removeCallbacks(it) }
|
||||
pendingResize = null
|
||||
lastWidth = 0; lastHeight = 0
|
||||
return original.onSurfaceTextureDestroyed(surface)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
flutterEngine.plugins.add(MpvPlayerPlugin())
|
||||
|
||||
@@ -217,7 +217,11 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
if (disposing || !isInitialized) return@post
|
||||
val container = FlutterOverlayHelper.findFlutterContainer(contentView, surfaceContainer)
|
||||
?: return@post
|
||||
FlutterOverlayHelper.configureFlutterZOrder(contentView, container, zOrderOnTop = false)
|
||||
// Fires on every layout pass via OnGlobalLayoutListener; skip when the
|
||||
// container is already at the front to avoid recursing the view tree
|
||||
// and re-writing compositionOrder each time.
|
||||
if (contentView.getChildAt(contentView.childCount - 1) === container) return@post
|
||||
FlutterOverlayHelper.configureFlutterZOrder(contentView, container, compositionOrder = 2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +234,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
child.setZOrderOnTop(false)
|
||||
child.setZOrderMediaOverlay(true)
|
||||
child.holder.setFormat(PixelFormat.TRANSLUCENT)
|
||||
FlutterOverlayHelper.applyCompositionOrder(child, 1)
|
||||
} else if (child is TextureView) {
|
||||
child.isOpaque = false
|
||||
}
|
||||
@@ -302,6 +307,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
holder.addCallback(surfaceCallback)
|
||||
setZOrderOnTop(false)
|
||||
setZOrderMediaOverlay(false)
|
||||
FlutterOverlayHelper.applyCompositionOrder(this, 0)
|
||||
}
|
||||
|
||||
videoAspectContainer!!.addView(surfaceView)
|
||||
@@ -325,9 +331,10 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
contentView.addView(surfaceContainer, 0)
|
||||
|
||||
// Find FlutterView and configure z-order
|
||||
// Video SurfaceView is at the bottom, Flutter uses setZOrderMediaOverlay to render above
|
||||
// Stack (top → bottom): Flutter UI (compositionOrder=2) > subtitle overlay
|
||||
// (1) > video (0). On pre-36 this falls back to legacy bucket ordering.
|
||||
FlutterOverlayHelper.findFlutterContainer(contentView, surfaceContainer)?.let { container ->
|
||||
FlutterOverlayHelper.configureFlutterZOrder(contentView, container, zOrderOnTop = false)
|
||||
FlutterOverlayHelper.configureFlutterZOrder(contentView, container, compositionOrder = 2)
|
||||
}
|
||||
|
||||
ensureFlutterOverlayOnTop()
|
||||
|
||||
@@ -77,7 +77,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
||||
flutterOverlayApplied = true
|
||||
return@post
|
||||
}
|
||||
FlutterOverlayHelper.configureFlutterZOrder(contentView, container, zOrderOnTop = true)
|
||||
FlutterOverlayHelper.configureFlutterZOrder(contentView, container, compositionOrder = 1)
|
||||
flutterOverlayApplied = true
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
||||
holder.addCallback(this@MpvPlayerCore)
|
||||
setZOrderOnTop(false)
|
||||
setZOrderMediaOverlay(false)
|
||||
FlutterOverlayHelper.applyCompositionOrder(this, 0)
|
||||
}
|
||||
|
||||
// Add SurfaceView to container
|
||||
@@ -163,9 +164,10 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
||||
val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
|
||||
contentView.addView(surfaceContainer, 0)
|
||||
|
||||
// Find FlutterView and set it on top of our video surface
|
||||
// Find FlutterView and set it on top of our video surface.
|
||||
// Stack (top → bottom): Flutter UI (compositionOrder=1) > video (0).
|
||||
FlutterOverlayHelper.findFlutterContainer(contentView, surfaceContainer)?.let { container ->
|
||||
FlutterOverlayHelper.configureFlutterZOrder(contentView, container, zOrderOnTop = true)
|
||||
FlutterOverlayHelper.configureFlutterZOrder(contentView, container, compositionOrder = 1)
|
||||
flutterOverlayApplied = true
|
||||
}
|
||||
ensureFlutterOverlayOnTop()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.edde746.plezy.shared
|
||||
|
||||
import android.graphics.PixelFormat
|
||||
import android.os.Build
|
||||
import android.view.SurfaceView
|
||||
import android.view.TextureView
|
||||
import android.view.View
|
||||
@@ -9,51 +10,70 @@ import android.view.ViewGroup
|
||||
object FlutterOverlayHelper {
|
||||
|
||||
/**
|
||||
* Find the FlutterView container in the content view hierarchy.
|
||||
* First tries to match by class name (works in debug builds),
|
||||
* then falls back to picking the last ViewGroup with children
|
||||
* (needed for release builds where FlutterView may be obfuscated).
|
||||
* Find the top-level container holding the Flutter render surface. Returns the
|
||||
* direct child of [contentView] that contains a FlutterSurfaceView/FlutterTextureView
|
||||
* at any depth — `bringChildToFront` only works for direct children, so this is the
|
||||
* node to pass to [configureFlutterZOrder].
|
||||
*
|
||||
* Searches any depth because an app may wrap Flutter (e.g. for key-event dispatch)
|
||||
* and push it below the direct-child level.
|
||||
*/
|
||||
fun findFlutterContainer(contentView: ViewGroup, excludeView: View? = null): ViewGroup? {
|
||||
// First pass: look for FlutterView by name (debug builds)
|
||||
for (i in 0 until contentView.childCount) {
|
||||
val child = contentView.getChildAt(i)
|
||||
if (child is ViewGroup && child.javaClass.name.contains("FlutterView")) {
|
||||
return child
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for release (FlutterView may be obfuscated): pick the last ViewGroup
|
||||
// that is not our video container and has children.
|
||||
for (i in contentView.childCount - 1 downTo 0) {
|
||||
val child = contentView.getChildAt(i)
|
||||
if (child is ViewGroup && child != excludeView && child.childCount > 0) {
|
||||
return child
|
||||
}
|
||||
if (child === excludeView || child !is ViewGroup) continue
|
||||
if (findRenderSurface(child) != null) return child
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun findRenderSurface(root: ViewGroup): View? {
|
||||
for (i in 0 until root.childCount) {
|
||||
val child = root.getChildAt(i)
|
||||
if (child is SurfaceView || child is TextureView) return child
|
||||
if (child is ViewGroup) findRenderSurface(child)?.let { return it }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure z-ordering so the Flutter UI renders above the video surface.
|
||||
*
|
||||
* @param zOrderOnTop true for MPV (Flutter needs to be fully on top),
|
||||
* false for ExoPlayer (subtitle layer sits between video and Flutter)
|
||||
* Apply [SurfaceView.setCompositionOrder] on API 36+; no-op on older APIs where
|
||||
* the legacy [SurfaceView.setZOrderOnTop]/[SurfaceView.setZOrderMediaOverlay]
|
||||
* bucket settings govern Z-order instead.
|
||||
*/
|
||||
fun configureFlutterZOrder(contentView: ViewGroup, container: ViewGroup, zOrderOnTop: Boolean) {
|
||||
fun applyCompositionOrder(view: SurfaceView, order: Int) {
|
||||
if (Build.VERSION.SDK_INT >= 36) view.compositionOrder = order
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure z-ordering so the Flutter UI renders above the video/subtitle surfaces.
|
||||
*
|
||||
* On API 36+ the value is applied via [SurfaceView.setCompositionOrder]: higher values
|
||||
* render above peers with lower values. On pre-36 SurfaceView builds the value is
|
||||
* mapped to the legacy on-top bucket when positive. On TextureView builds the value
|
||||
* is unused (view hierarchy order handles it).
|
||||
*/
|
||||
fun configureFlutterZOrder(contentView: ViewGroup, container: ViewGroup, compositionOrder: Int) {
|
||||
contentView.bringChildToFront(container)
|
||||
for (j in 0 until container.childCount) {
|
||||
val flutterChild = container.getChildAt(j)
|
||||
if (flutterChild is SurfaceView) {
|
||||
flutterChild.setZOrderOnTop(zOrderOnTop)
|
||||
flutterChild.setZOrderMediaOverlay(true)
|
||||
flutterChild.holder.setFormat(PixelFormat.TRANSLUCENT)
|
||||
break
|
||||
} else if (flutterChild is TextureView) {
|
||||
flutterChild.isOpaque = false
|
||||
break
|
||||
when (val surface = findRenderSurface(container)) {
|
||||
is SurfaceView -> {
|
||||
if (Build.VERSION.SDK_INT >= 36) {
|
||||
// Clear legacy bucket hints so compositionOrder is authoritative.
|
||||
// Flutter's FlutterSurfaceView sets setZOrderOnTop(true) in its
|
||||
// transparent-mode constructor, which otherwise pins it to z=1.
|
||||
surface.setZOrderOnTop(false)
|
||||
surface.setZOrderMediaOverlay(false)
|
||||
surface.compositionOrder = compositionOrder
|
||||
} else {
|
||||
// Pre-36 has 3 coarse sublayer buckets. Put Flutter in the on-top
|
||||
// bucket so it renders above the video (default) and libass subtitle
|
||||
// (media overlay) SurfaceViews. NB: setZOrderMediaOverlay overwrites
|
||||
// mSubLayer internally, so don't call it here or it cancels setZOrderOnTop.
|
||||
surface.setZOrderOnTop(compositionOrder > 0)
|
||||
}
|
||||
surface.holder.setFormat(PixelFormat.TRANSLUCENT)
|
||||
}
|
||||
is TextureView -> surface.isOpaque = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user