feat(android): flutter surfaceview rendering
This commit is contained in:
@@ -1,12 +1,9 @@
|
|||||||
package com.edde746.plezy
|
package com.edde746.plezy
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.SurfaceTexture
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.app.AppOpsManager
|
import android.app.AppOpsManager
|
||||||
import android.app.PictureInPictureParams
|
import android.app.PictureInPictureParams
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -14,14 +11,12 @@ import android.content.res.Configuration
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Rational
|
import android.util.Rational
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.TextureView
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterShellArgs
|
import io.flutter.embedding.engine.FlutterShellArgs
|
||||||
import io.flutter.embedding.android.FlutterTextureView
|
|
||||||
import io.flutter.embedding.android.RenderMode
|
import io.flutter.embedding.android.RenderMode
|
||||||
import io.flutter.embedding.android.TransparencyMode
|
import io.flutter.embedding.android.TransparencyMode
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
@@ -149,51 +144,13 @@ class MainActivity : FlutterActivity() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getRenderMode(): RenderMode {
|
override fun getRenderMode(): RenderMode = RenderMode.surface
|
||||||
// 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 getTransparencyMode(): TransparencyMode {
|
override fun getTransparencyMode(): TransparencyMode {
|
||||||
// Keep Flutter transparent so video/subtitles are visible below.
|
// Keep Flutter transparent so video/subtitles are visible below.
|
||||||
return TransparencyMode.transparent
|
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) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
flutterEngine.plugins.add(MpvPlayerPlugin())
|
flutterEngine.plugins.add(MpvPlayerPlugin())
|
||||||
|
|||||||
@@ -217,7 +217,11 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
|||||||
if (disposing || !isInitialized) return@post
|
if (disposing || !isInitialized) return@post
|
||||||
val container = FlutterOverlayHelper.findFlutterContainer(contentView, surfaceContainer)
|
val container = FlutterOverlayHelper.findFlutterContainer(contentView, surfaceContainer)
|
||||||
?: return@post
|
?: 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.setZOrderOnTop(false)
|
||||||
child.setZOrderMediaOverlay(true)
|
child.setZOrderMediaOverlay(true)
|
||||||
child.holder.setFormat(PixelFormat.TRANSLUCENT)
|
child.holder.setFormat(PixelFormat.TRANSLUCENT)
|
||||||
|
FlutterOverlayHelper.applyCompositionOrder(child, 1)
|
||||||
} else if (child is TextureView) {
|
} else if (child is TextureView) {
|
||||||
child.isOpaque = false
|
child.isOpaque = false
|
||||||
}
|
}
|
||||||
@@ -302,6 +307,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
|||||||
holder.addCallback(surfaceCallback)
|
holder.addCallback(surfaceCallback)
|
||||||
setZOrderOnTop(false)
|
setZOrderOnTop(false)
|
||||||
setZOrderMediaOverlay(false)
|
setZOrderMediaOverlay(false)
|
||||||
|
FlutterOverlayHelper.applyCompositionOrder(this, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
videoAspectContainer!!.addView(surfaceView)
|
videoAspectContainer!!.addView(surfaceView)
|
||||||
@@ -325,9 +331,10 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
|||||||
contentView.addView(surfaceContainer, 0)
|
contentView.addView(surfaceContainer, 0)
|
||||||
|
|
||||||
// Find FlutterView and configure z-order
|
// 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.findFlutterContainer(contentView, surfaceContainer)?.let { container ->
|
||||||
FlutterOverlayHelper.configureFlutterZOrder(contentView, container, zOrderOnTop = false)
|
FlutterOverlayHelper.configureFlutterZOrder(contentView, container, compositionOrder = 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureFlutterOverlayOnTop()
|
ensureFlutterOverlayOnTop()
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
|||||||
flutterOverlayApplied = true
|
flutterOverlayApplied = true
|
||||||
return@post
|
return@post
|
||||||
}
|
}
|
||||||
FlutterOverlayHelper.configureFlutterZOrder(contentView, container, zOrderOnTop = true)
|
FlutterOverlayHelper.configureFlutterZOrder(contentView, container, compositionOrder = 1)
|
||||||
flutterOverlayApplied = true
|
flutterOverlayApplied = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,6 +154,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
|||||||
holder.addCallback(this@MpvPlayerCore)
|
holder.addCallback(this@MpvPlayerCore)
|
||||||
setZOrderOnTop(false)
|
setZOrderOnTop(false)
|
||||||
setZOrderMediaOverlay(false)
|
setZOrderMediaOverlay(false)
|
||||||
|
FlutterOverlayHelper.applyCompositionOrder(this, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add SurfaceView to container
|
// 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)
|
val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
|
||||||
contentView.addView(surfaceContainer, 0)
|
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.findFlutterContainer(contentView, surfaceContainer)?.let { container ->
|
||||||
FlutterOverlayHelper.configureFlutterZOrder(contentView, container, zOrderOnTop = true)
|
FlutterOverlayHelper.configureFlutterZOrder(contentView, container, compositionOrder = 1)
|
||||||
flutterOverlayApplied = true
|
flutterOverlayApplied = true
|
||||||
}
|
}
|
||||||
ensureFlutterOverlayOnTop()
|
ensureFlutterOverlayOnTop()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.edde746.plezy.shared
|
package com.edde746.plezy.shared
|
||||||
|
|
||||||
import android.graphics.PixelFormat
|
import android.graphics.PixelFormat
|
||||||
|
import android.os.Build
|
||||||
import android.view.SurfaceView
|
import android.view.SurfaceView
|
||||||
import android.view.TextureView
|
import android.view.TextureView
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -9,51 +10,70 @@ import android.view.ViewGroup
|
|||||||
object FlutterOverlayHelper {
|
object FlutterOverlayHelper {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the FlutterView container in the content view hierarchy.
|
* Find the top-level container holding the Flutter render surface. Returns the
|
||||||
* First tries to match by class name (works in debug builds),
|
* direct child of [contentView] that contains a FlutterSurfaceView/FlutterTextureView
|
||||||
* then falls back to picking the last ViewGroup with children
|
* at any depth — `bringChildToFront` only works for direct children, so this is the
|
||||||
* (needed for release builds where FlutterView may be obfuscated).
|
* 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? {
|
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) {
|
for (i in contentView.childCount - 1 downTo 0) {
|
||||||
val child = contentView.getChildAt(i)
|
val child = contentView.getChildAt(i)
|
||||||
if (child is ViewGroup && child != excludeView && child.childCount > 0) {
|
if (child === excludeView || child !is ViewGroup) continue
|
||||||
return child
|
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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure z-ordering so the Flutter UI renders above the video surface.
|
* Apply [SurfaceView.setCompositionOrder] on API 36+; no-op on older APIs where
|
||||||
*
|
* the legacy [SurfaceView.setZOrderOnTop]/[SurfaceView.setZOrderMediaOverlay]
|
||||||
* @param zOrderOnTop true for MPV (Flutter needs to be fully on top),
|
* bucket settings govern Z-order instead.
|
||||||
* false for ExoPlayer (subtitle layer sits between video and Flutter)
|
|
||||||
*/
|
*/
|
||||||
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)
|
contentView.bringChildToFront(container)
|
||||||
for (j in 0 until container.childCount) {
|
when (val surface = findRenderSurface(container)) {
|
||||||
val flutterChild = container.getChildAt(j)
|
is SurfaceView -> {
|
||||||
if (flutterChild is SurfaceView) {
|
if (Build.VERSION.SDK_INT >= 36) {
|
||||||
flutterChild.setZOrderOnTop(zOrderOnTop)
|
// Clear legacy bucket hints so compositionOrder is authoritative.
|
||||||
flutterChild.setZOrderMediaOverlay(true)
|
// Flutter's FlutterSurfaceView sets setZOrderOnTop(true) in its
|
||||||
flutterChild.holder.setFormat(PixelFormat.TRANSLUCENT)
|
// transparent-mode constructor, which otherwise pins it to z=1.
|
||||||
break
|
surface.setZOrderOnTop(false)
|
||||||
} else if (flutterChild is TextureView) {
|
surface.setZOrderMediaOverlay(false)
|
||||||
flutterChild.isOpaque = false
|
surface.compositionOrder = compositionOrder
|
||||||
break
|
} 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