Mobile Development 18 min read

Why Flutter’s PlatformView Can Freeze Video: Deep Engine Bug Analysis & Fixes

This article examines a video rendering bug in Flutter when combining Texture widgets with PlatformView using Hybrid Composition, analyzes the underlying engine and rendering pipeline across Android, and presents three concrete solutions—including engine patches and Dart API tweaks—to restore proper texture refresh.

Inke Technology
Inke Technology
Inke Technology
Why Flutter’s PlatformView Can Freeze Video: Deep Engine Bug Analysis & Fixes

Understanding the PlatformView and External Texture Bug in Flutter

When developing Flutter products, native components are often required for video rendering or web browsing. Instead of re‑implementing these capabilities in Flutter, developers rely on PlatformView or external textures. This article uses a native component rendering bug to explore the rendering principles of PlatformView.

Analysis is based on Flutter Android source code.

Readers with Flutter experience will find the discussion clearer.

Bug Overview

Using a Texture widget together with a PlatformView (Hybrid Composition via SurfaceAndroidView ) causes the texture to stop refreshing, appearing as a frozen frame. Removing the PlatformView restores normal refresh. The issue also occurs with video + WebView (also using SurfaceAndroidView ).

The problem originates from layer mixing between Texture and PlatformView on Android; iOS does not exhibit this behavior.

PlatformView Evolution

VirtualDisplay

Initially, Flutter used AndroidView with VirtualDisplay . When AndroidViewSurface is used, a hybrid=true flag triggers Hybrid Composition. The older VirtualDisplay approach renders the native view onto a Surface created by the system, requiring extra gesture forwarding.

HybridComposition

From Flutter 1.20, Hybrid Composition renders native components together with the Flutter canvas via SurfaceAndroidView . The native view is added to FlutterView using FlutterMutorView (a FrameLayout ) and mixed via FlutterImageView . This method is more stable than VirtualDisplay but introduces complex layer composition.

Flutter 3.0 New Rendering Scheme

Flutter 3.0 replaces VirtualDisplay with a new approach: native components are added to FlutterView via PlatformViewWrapper , which draws to FlutterSurfaceTexture and uses surface.lockHardwareCanvas() to reduce CPU copies. This improves performance and compatibility.

External Texture Refresh Process

When a Texture widget receives a texture ID, the native side creates a SurfaceTexture , registers it with the Flutter engine, and the engine marks the texture frame as available via OnFrameAvailableListener . The Java callback notifies the native side, which then schedules a frame without rebuilding the layer tree.

<code>public SurfaceTextureEntry createSurfaceTexture() {
    final SurfaceTexture surfaceTexture = new SurfaceTexture(0);
    surfaceTexture.detachFromGLContext();
    final SurfaceTextureRegistryEntry entry = new SurfaceTextureRegistryEntry(nextTextureId.getAndIncrement(), surfaceTexture);
    // Register with engine
    registerTexture(entry.id(), entry.textureWrapper());
    return entry;
}

private native void nativeRegisterTexture(
    long nativeShellHolderId, long textureId, @NonNull SurfaceTextureWrapper textureWrapper);
</code>

The native onFrameAvailable listener marks the texture as having a new frame and triggers ScheduleFrame(false) to refresh the display.

<code>private SurfaceTexture.OnFrameAvailableListener onFrameListener =
    new SurfaceTexture.OnFrameAvailableListener() {
      @Override
      public void onFrameAvailable(SurfaceTexture texture) {
        // Notify native that frame is ready
        mNativeView.getFlutterJNI().markTextureFrameAvailable(SurfaceTextureRegistryEntry.this.id);
      }
    };

// Native method to inform the engine
private native void nativeMarkTextureFrameAvailable(long nativeShellHolderId, long textureId);
</code>

ScheduleFrame(false) Flow

When ScheduleFrame(false) is called, the engine re‑uses the previous layer tree ( last_layer_tree_ ) without rebuilding the UI, which is why texture refresh is efficient.

ScheduleFrame(true) Flow

Normal setState() triggers engine→ScheduleFrame() with true , rebuilding the layer tree.

Thus, Texture refresh only triggers layer composition in the engine, not a full UI rebuild.

Dart Layer Implementation

<code>class TextureBox extends RenderBox {
  @override
  bool get alwaysNeedsCompositing => true;

  @override
  Size computeDryLayout(BoxConstraints constraints) {
    return constraints.biggest;
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    context.addLayer(TextureLayer(
      rect: Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height),
      textureId: _textureId,
      freeze: freeze,
      filterQuality: _filterQuality,
    ));
  }
}
</code>

Engine Layer Implementation

<code>void TextureLayer::Paint(PaintContext& context) const {
  TRACE_EVENT0("flutter", "TextureLayer::Paint");
  std::shared_ptr<Texture> texture = context.texture_registry.GetTexture(texture_id_);
  texture->Paint(*context.leaf_nodes_canvas, paint_bounds(), freeze_, context.gr_context, sampling_);
}
</code>

On Android, the concrete implementation AndroidExternalTextureGL creates a SkImage from the external OES texture and draws it onto the canvas.

<code>void AndroidExternalTextureGL::Paint(SkCanvas& canvas,
                                     const SkRect& bounds,
                                     bool freeze,
                                     GrDirectContext* context,
                                     const SkSamplingOptions& sampling) {
  GrGLTextureInfo textureInfo = {GL_TEXTURE_EXTERNAL_OES, texture_name_, GL_RGBA8_OES};
  GrBackendTexture backendTexture(1, 1, GrMipMapped::kNo, textureInfo);
  sk_sp<SkImage> image = SkImage::MakeFromTexture(
      context, backendTexture, kTopLeft_GrSurfaceOrigin, kRGBA_8888_SkColorType,
      kPremul_SkAlphaType, nullptr);
  if (image) {
    SkAutoCanvasRestore autoRestore(&canvas, true);
    canvas.translate(bounds.x(), bounds.y());
    canvas.scale(bounds.width(), bounds.height());
    if (!transform.isIdentity()) {
      SkMatrix transformAroundCenter(transform);
      transformAroundCenter.preTranslate(-0.5, -0.5);
      transformAroundCenter.postScale(1, -1);
      transformAroundCenter.postTranslate(0.5, 0.5);
      canvas.concat(transformAroundCenter);
    }
    canvas.drawImage(image, 0, 0, sampling, nullptr);
  }
}
</code>

Root Cause Analysis

The bug occurs because, when using Hybrid Composition, the engine converts the render surface from SurfaceView to FlutterImageView . The onEndFrame callback, which should notify FlutterImageView of the latest frame, is not invoked when the layer tree does not change, preventing texture updates.

Fixes

Solution 1 (Engine Patch) Add an onEndFrame call after rasterization in Rasterizer::DrawToSurface to ensure texture frames are marked. <code>RasterStatus Rasterizer::DrawToSurface(...){ // ... if (external_view_embedder_) { external_view_embedder_->EndFrame(should_resubmit_frame, raster_thread_merger_); } } </code>

Solution 2 (Java Hook) Invoke acquireLatestImage at the end of PlatformViewsController.onDisplayPlatformView() . Simple but deviates from API design.

Solution 3 (Recommended – Dart API) Disable the surface conversion by calling PlatformViewsService.synchronizeToNativeViewHierarchy(false) from Dart, preventing the loss of onEndFrame callbacks. <code>static Future<void> synchronizeToNativeViewHierarchy(bool yes) { assert(defaultTargetPlatform == TargetPlatform.android); return SystemChannels.platform_views.invokeMethod<void>( 'synchronizeToNativeViewHierarchy', yes); } </code>

References

Exploration of the Flutter Rendering Mechanism from Architecture to Source Code

Flutter 深入探索混合开发的技术演进

Compiling the engine

FlutterrenderingAndroidtextureBug Fixplatformviewhybrid-composition
Inke Technology
Written by

Inke Technology

Official account of Inke Technology

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.