fix(android): hole-punch video and libass surfaces on API 36+
This commit is contained in:
@@ -221,7 +221,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
// 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)
|
||||
FlutterOverlayHelper.configureFlutterZOrder(contentView, container, compositionOrder = 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +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)
|
||||
FlutterOverlayHelper.applyCompositionOrder(child, -1)
|
||||
} else if (child is TextureView) {
|
||||
child.isOpaque = false
|
||||
}
|
||||
@@ -307,7 +307,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
holder.addCallback(surfaceCallback)
|
||||
setZOrderOnTop(false)
|
||||
setZOrderMediaOverlay(false)
|
||||
FlutterOverlayHelper.applyCompositionOrder(this, 0)
|
||||
FlutterOverlayHelper.applyCompositionOrder(this, -2)
|
||||
}
|
||||
|
||||
videoAspectContainer!!.addView(surfaceView)
|
||||
@@ -330,11 +330,15 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
|
||||
contentView.addView(surfaceContainer, 0)
|
||||
|
||||
// Find FlutterView and configure z-order
|
||||
// Stack (top → bottom): Flutter UI (compositionOrder=2) > subtitle overlay
|
||||
// (1) > video (0). On pre-36 this falls back to legacy bucket ordering.
|
||||
// Find FlutterView and configure z-order. compositionOrder maps directly to
|
||||
// SurfaceView mSubLayer on API 36+: negative values are hole-punched behind
|
||||
// the parent canvas, non-negative are composited above. Media3's
|
||||
// CanvasSubtitleOutput renders SRT/VTT/SDH text on the parent canvas, so the
|
||||
// video and libass surfaces must be negative for non-ASS subs to be visible.
|
||||
// Stack (back → front): video (-2) → libass overlay (-1) → parent canvas
|
||||
// (CanvasSubtitleOutput) → Flutter UI (+1). Pre-36 falls back to legacy buckets.
|
||||
FlutterOverlayHelper.findFlutterContainer(contentView, surfaceContainer)?.let { container ->
|
||||
FlutterOverlayHelper.configureFlutterZOrder(contentView, container, compositionOrder = 2)
|
||||
FlutterOverlayHelper.configureFlutterZOrder(contentView, container, compositionOrder = 1)
|
||||
}
|
||||
|
||||
ensureFlutterOverlayOnTop()
|
||||
@@ -490,8 +494,8 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
// Add ASS overlay view to SubtitleView for OVERLAY modes.
|
||||
// We use AssSubtitleSurfaceView directly (not AssSubtitleView) so we get a
|
||||
// SurfaceFlinger-layer-backed overlay that eglPresentationTimeANDROID can
|
||||
// vsync-pin. Z-order: default video SurfaceView < this MediaOverlay-flagged
|
||||
// SurfaceView < Flutter TextureView in the window.
|
||||
// vsync-pin. Z-order: video SurfaceView (-2) < this MediaOverlay-flagged
|
||||
// SurfaceView (-1) < parent canvas < Flutter SurfaceView (+1) in the window.
|
||||
//
|
||||
// Inserted at child index 0 so the SurfaceView's transparent punch runs BEFORE
|
||||
// SubtitleView's built-in CanvasSubtitleOutput child renders non-ASS cues.
|
||||
|
||||
@@ -154,7 +154,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
||||
holder.addCallback(this@MpvPlayerCore)
|
||||
setZOrderOnTop(false)
|
||||
setZOrderMediaOverlay(false)
|
||||
FlutterOverlayHelper.applyCompositionOrder(this, 0)
|
||||
FlutterOverlayHelper.applyCompositionOrder(this, -2)
|
||||
}
|
||||
|
||||
// Add SurfaceView to container
|
||||
@@ -165,7 +165,9 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
||||
contentView.addView(surfaceContainer, 0)
|
||||
|
||||
// Find FlutterView and set it on top of our video surface.
|
||||
// Stack (top → bottom): Flutter UI (compositionOrder=1) > video (0).
|
||||
// compositionOrder maps directly to SurfaceView mSubLayer on API 36+:
|
||||
// negative is hole-punched behind the parent canvas, non-negative is above.
|
||||
// Stack (back → front): video (-2, hole-punched) → parent canvas → Flutter UI (+1).
|
||||
FlutterOverlayHelper.findFlutterContainer(contentView, surfaceContainer)?.let { container ->
|
||||
FlutterOverlayHelper.configureFlutterZOrder(contentView, container, compositionOrder = 1)
|
||||
flutterOverlayApplied = true
|
||||
|
||||
@@ -40,6 +40,15 @@ object FlutterOverlayHelper {
|
||||
* Apply [SurfaceView.setCompositionOrder] on API 36+; no-op on older APIs where
|
||||
* the legacy [SurfaceView.setZOrderOnTop]/[SurfaceView.setZOrderMediaOverlay]
|
||||
* bucket settings govern Z-order instead.
|
||||
*
|
||||
* The value maps directly to the SurfaceView's `mSubLayer`. Per AOSP semantics:
|
||||
* `mSubLayer >= 0` (non-negative `order`) is composited above the parent window
|
||||
* and skips the hole-punch in `draw()`; `mSubLayer < 0` (negative `order`) is
|
||||
* behind the parent window and the SurfaceView is made visible by punching a
|
||||
* transparent hole in the parent canvas. Valid range is `[-2, 2]`. Picking the
|
||||
* right sign matters: any view that draws on the parent canvas (e.g. Media3's
|
||||
* `CanvasSubtitleOutput` for SRT/VTT/SDH) is hidden by SurfaceViews with
|
||||
* non-negative orders that sit on top of the same area.
|
||||
*/
|
||||
fun applyCompositionOrder(view: SurfaceView, order: Int) {
|
||||
if (Build.VERSION.SDK_INT >= 36) view.compositionOrder = order
|
||||
@@ -48,10 +57,14 @@ object FlutterOverlayHelper {
|
||||
/**
|
||||
* 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).
|
||||
* On API 36+ the value is applied via [SurfaceView.setCompositionOrder] and maps
|
||||
* directly to `mSubLayer` (see [applyCompositionOrder] for the sign semantics).
|
||||
* 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).
|
||||
*
|
||||
* Pass a non-negative value (e.g. `1`) so Flutter renders above the parent canvas
|
||||
* and lets the legacy transparent-mode `setZOrderOnTop(true)` semantics carry over.
|
||||
*/
|
||||
fun configureFlutterZOrder(contentView: ViewGroup, container: ViewGroup, compositionOrder: Int) {
|
||||
contentView.bringChildToFront(container)
|
||||
|
||||
Reference in New Issue
Block a user