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.
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
Inke Technology
Official account of Inke Technology
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.