Flutter Engine
The Flutter Engine
ExternalTextureFlutterActivity.java
Go to the documentation of this file.
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package dev.flutter.scenarios;
6
7import static io.flutter.Build.API_LEVELS;
8
9import android.content.res.AssetFileDescriptor;
10import android.graphics.Canvas;
11import android.graphics.ImageFormat;
12import android.graphics.LinearGradient;
13import android.graphics.Paint;
14import android.graphics.Shader.TileMode;
15import android.hardware.HardwareBuffer;
16import android.media.Image;
17import android.media.ImageReader;
18import android.media.ImageWriter;
19import android.media.MediaCodec;
20import android.media.MediaExtractor;
21import android.media.MediaFormat;
22import android.os.Build.VERSION;
23import android.os.Bundle;
24import android.os.Handler;
25import android.os.HandlerThread;
26import android.util.Log;
27import android.view.Gravity;
28import android.view.Surface;
29import android.view.SurfaceHolder;
30import android.view.SurfaceView;
31import android.view.ViewGroup;
32import android.widget.FrameLayout;
33import android.widget.FrameLayout.LayoutParams;
34import androidx.annotation.NonNull;
35import androidx.annotation.Nullable;
36import androidx.annotation.RequiresApi;
37import androidx.core.util.Supplier;
38import io.flutter.view.TextureRegistry;
39import java.io.IOException;
40import java.nio.ByteBuffer;
41import java.util.Map;
42import java.util.Objects;
43import java.util.concurrent.CountDownLatch;
44
46 static final String TAG = "Scenarios";
47 private static final int SURFACE_WIDTH = 192;
48 private static final int SURFACE_HEIGHT = 256;
49
50 private SurfaceRenderer flutterRenderer;
51
52 // Latch used to ensure both SurfaceRenderers produce a frame before taking a screenshot.
53 private final CountDownLatch firstFrameLatch = new CountDownLatch(1);
54
55 private long textureId = 0;
56 private TextureRegistry.SurfaceProducer surfaceProducer;
57
58 @Override
59 protected void onCreate(@Nullable Bundle savedInstanceState) {
60 super.onCreate(savedInstanceState);
61
62 String surfaceRenderer = getIntent().getStringExtra("surface_renderer");
63 assert surfaceRenderer != null;
64 flutterRenderer = selectSurfaceRenderer(surfaceRenderer);
65
66 // Create and place a SurfaceView above the Flutter content.
67 SurfaceView surfaceView = new SurfaceView(getContext());
68 surfaceView.setZOrderMediaOverlay(true);
69 surfaceView.setMinimumWidth(SURFACE_WIDTH);
70 surfaceView.setMinimumHeight(SURFACE_HEIGHT);
71
72 FrameLayout frameLayout = new FrameLayout(getContext());
73 frameLayout.addView(
74 surfaceView,
75 new LayoutParams(
76 ViewGroup.LayoutParams.WRAP_CONTENT,
77 ViewGroup.LayoutParams.WRAP_CONTENT,
78 Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL));
79
80 addContentView(
81 frameLayout,
82 new ViewGroup.LayoutParams(
83 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
84
85 SurfaceHolder surfaceHolder = surfaceView.getHolder();
86 surfaceHolder.setFixedSize(SURFACE_WIDTH, SURFACE_HEIGHT);
87 }
88
89 @Override
91 super.waitUntilFlutterRendered();
92
93 try {
94 firstFrameLatch.await();
95 } catch (InterruptedException e) {
96 throw new RuntimeException(e);
97 }
98 }
99
100 private SurfaceRenderer selectSurfaceRenderer(String surfaceRenderer) {
101 switch (surfaceRenderer) {
102 case "image":
103 if (VERSION.SDK_INT >= API_LEVELS.API_23) {
104 // CanvasSurfaceRenderer doesn't work correctly when used with ImageSurfaceRenderer.
105 // Use MediaSurfaceRenderer for now.
106 return new ImageSurfaceRenderer(selectSurfaceRenderer("media"));
107 } else {
108 throw new RuntimeException("ImageSurfaceRenderer not supported");
109 }
110 case "media":
111 return new MediaSurfaceRenderer(this::createMediaExtractor);
112 case "canvas":
113 default:
114 return new CanvasSurfaceRenderer();
115 }
116 }
117
118 private MediaExtractor createMediaExtractor() {
119 // Sample Video generated with FFMPEG.
120 // ffmpeg -loop 1 -i ~/engine/src/flutter/lib/ui/fixtures/DashInNooglerHat.jpg -c:v libx264
121 // -profile:v main -level:v 5.2 -t 1 -r 1 -vf scale=192:256 -b:v 1M sample.mp4
122 try {
123 MediaExtractor extractor = new MediaExtractor();
124 try (AssetFileDescriptor afd = getAssets().openFd("sample.mp4")) {
125 extractor.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
126 }
127 return extractor;
128 } catch (IOException e) {
129 e.printStackTrace();
130 throw new RuntimeException(e);
131 }
132 }
133
134 @Override
135 public void onPause() {
136 flutterRenderer.destroy();
137 surfaceProducer.release();
138 super.onPause();
139 }
140
141 @Override
142 public void onFlutterUiDisplayed() {
143 surfaceProducer =
144 Objects.requireNonNull(getFlutterEngine()).getRenderer().createSurfaceProducer();
145 surfaceProducer.setSize(SURFACE_WIDTH, SURFACE_HEIGHT);
146 flutterRenderer.attach(surfaceProducer.getSurface(), firstFrameLatch);
147 flutterRenderer.repaint();
148 textureId = surfaceProducer.id();
149
150 super.onFlutterUiDisplayed();
151 }
152
153 @Override
154 protected void getScenarioParams(@NonNull Map<String, Object> args) {
155 super.getScenarioParams(args);
156 args.put("texture_id", textureId);
157 args.put("texture_width", SURFACE_WIDTH);
158 args.put("texture_height", SURFACE_HEIGHT);
159 }
160
161 private interface SurfaceRenderer {
162 void attach(Surface surface, CountDownLatch onFirstFrame);
163
164 void repaint();
165
166 void destroy();
167 }
168
169 /** Paints a simple gradient onto the attached Surface. */
170 private static class CanvasSurfaceRenderer implements SurfaceRenderer {
171 private Surface surface;
172 private CountDownLatch onFirstFrame;
173
174 protected CanvasSurfaceRenderer() {}
175
176 @Override
177 public void attach(Surface surface, CountDownLatch onFirstFrame) {
178 this.surface = surface;
179 this.onFirstFrame = onFirstFrame;
180 }
181
182 @Override
183 public void repaint() {
184 Canvas canvas =
185 VERSION.SDK_INT >= API_LEVELS.API_23
186 ? surface.lockHardwareCanvas()
187 : surface.lockCanvas(null);
188 Paint paint = new Paint();
189 paint.setShader(
190 new LinearGradient(
191 0.0f,
192 0.0f,
193 canvas.getWidth(),
194 canvas.getHeight(),
195 new int[] {
196 // Cyan (#00FFFF)
197 0xFF00FFFF,
198 // Magenta (#FF00FF)
199 0xFFFF00FF,
200 // Yellow (#FFFF00)
201 0xFFFFFF00,
202 },
203 null,
204 TileMode.REPEAT));
205 canvas.drawPaint(paint);
206 surface.unlockCanvasAndPost(canvas);
207
208 if (onFirstFrame != null) {
209 onFirstFrame.countDown();
210 onFirstFrame = null;
211 }
212 }
213
214 @Override
215 public void destroy() {}
216 }
217
218 /** Decodes a sample video into the attached Surface. */
219 private static class MediaSurfaceRenderer implements SurfaceRenderer {
220 private final Supplier<MediaExtractor> extractorSupplier;
221 private CountDownLatch onFirstFrame;
222
223 private Surface surface;
224 private MediaExtractor extractor;
225 private MediaFormat format;
226 private Thread decodeThread;
227
228 protected MediaSurfaceRenderer(Supplier<MediaExtractor> extractorSupplier) {
229 this.extractorSupplier = extractorSupplier;
230 }
231
232 @Override
233 public void attach(Surface surface, CountDownLatch onFirstFrame) {
234 this.surface = surface;
235 this.onFirstFrame = onFirstFrame;
236
237 extractor = extractorSupplier.get();
238 format = extractor.getTrackFormat(0);
239
240 decodeThread = new Thread(this::decodeThreadMain);
241 decodeThread.start();
242 }
243
244 private void decodeThreadMain() {
245 try {
246 MediaCodec codec =
247 MediaCodec.createDecoderByType(
248 Objects.requireNonNull(format.getString(MediaFormat.KEY_MIME)));
249 codec.configure(format, surface, null, 0);
250 codec.start();
251
252 // Track 0 is always the video track, since the sample video doesn't contain audio.
253 extractor.selectTrack(0);
254
255 MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
256 boolean seenEOS = false;
257 long startTimeNs = System.nanoTime();
258 int frameCount = 0;
259
260 while (true) {
261 // Move samples (video frames) from the extractor into the decoder, as long as we haven't
262 // consumed all samples.
263 if (!seenEOS) {
264 int inputBufferIndex = codec.dequeueInputBuffer(-1);
265 ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferIndex);
266 assert inputBuffer != null;
267 int sampleSize = extractor.readSampleData(inputBuffer, 0);
268 if (sampleSize >= 0) {
269 long presentationTimeUs = extractor.getSampleTime();
270 codec.queueInputBuffer(inputBufferIndex, 0, sampleSize, presentationTimeUs, 0);
271 extractor.advance();
272 } else {
273 codec.queueInputBuffer(
274 inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
275 seenEOS = true;
276 }
277 }
278
279 // Then consume decoded video frames from the decoder. These frames are automatically
280 // pushed to the attached Surface, so this schedules them for present.
281 int outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 10000);
282 boolean lastBuffer = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
283 if (outputBufferIndex >= 0) {
284 if (bufferInfo.size > 0) {
285 if (onFirstFrame != null) {
286 onFirstFrame.countDown();
287 onFirstFrame = null;
288 }
289 Log.w(TAG, "Presenting frame " + frameCount);
290 frameCount++;
291
292 codec.releaseOutputBuffer(
293 outputBufferIndex, startTimeNs + (bufferInfo.presentationTimeUs * 1000));
294 }
295 }
296
297 // Exit the loop if there are no more frames available.
298 if (lastBuffer) {
299 break;
300 }
301 }
302
303 codec.stop();
304 codec.release();
305 extractor.release();
306 } catch (IOException e) {
307 e.printStackTrace();
308 throw new RuntimeException(e);
309 }
310 }
311
312 @Override
313 public void repaint() {}
314
315 @Override
316 public void destroy() {
317 try {
318 decodeThread.join();
319 } catch (InterruptedException e) {
320 e.printStackTrace();
321 throw new RuntimeException(e);
322 }
323 }
324 }
325
326 /**
327 * Takes frames from the inner SurfaceRenderer and feeds it through an ImageReader and ImageWriter
328 * pair.
329 */
330 @RequiresApi(API_LEVELS.API_23)
331 private static class ImageSurfaceRenderer implements SurfaceRenderer {
332 private final SurfaceRenderer inner;
333 private CountDownLatch onFirstFrame;
334 private ImageReader reader;
335 private ImageWriter writer;
336
337 private Handler handler;
338 private HandlerThread handlerThread;
339
340 private boolean canReadImage = true;
341 private boolean canWriteImage = true;
342
343 protected ImageSurfaceRenderer(SurfaceRenderer inner) {
344 this.inner = inner;
345 }
346
347 @Override
348 public void attach(Surface surface, CountDownLatch onFirstFrame) {
349 this.onFirstFrame = onFirstFrame;
350 if (VERSION.SDK_INT >= API_LEVELS.API_29) {
351 // On Android Q+, use PRIVATE image format.
352 // Also let the frame producer know the images will
353 // be sampled from by the GPU.
354 writer = ImageWriter.newInstance(surface, 3, ImageFormat.PRIVATE);
355 reader =
356 ImageReader.newInstance(
357 SURFACE_WIDTH,
358 SURFACE_HEIGHT,
359 ImageFormat.PRIVATE,
360 2,
361 HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
362 } else {
363 // Before Android Q, this will change the format of the surface to match the images.
364 writer = ImageWriter.newInstance(surface, 3);
365 reader = ImageReader.newInstance(SURFACE_WIDTH, SURFACE_HEIGHT, writer.getFormat(), 2);
366 }
367 inner.attach(reader.getSurface(), null);
368
369 handlerThread = new HandlerThread("image reader/writer thread");
370 handlerThread.start();
371
372 handler = new Handler(handlerThread.getLooper());
373 reader.setOnImageAvailableListener(this::onImageAvailable, handler);
374 writer.setOnImageReleasedListener(this::onImageReleased, handler);
375 }
376
377 private void onImageAvailable(ImageReader reader) {
378 Log.v(TAG, "Image available");
379
380 if (!canWriteImage) {
381 // If the ImageWriter hasn't released the latest image, don't attempt to enqueue another
382 // image.
383 // Otherwise the reader writer pair locks up if the writer runs behind, as the reader runs
384 // out of images and the writer has no more space for images.
385 canReadImage = true;
386 return;
387 }
388
389 canReadImage = false;
390 Image image = reader.acquireLatestImage();
391 try {
392 canWriteImage = false;
393 writer.queueInputImage(image);
394 } catch (IllegalStateException e) {
395 // If the output surface disconnects, this method will be interrupted with an
396 // IllegalStateException.
397 // Simply log and return.
398 Log.i(TAG, "Surface disconnected from ImageWriter", e);
399 image.close();
400 }
401
402 Log.v(TAG, "Output image");
403
404 if (onFirstFrame != null) {
405 onFirstFrame.countDown();
406 onFirstFrame = null;
407 }
408 }
409
410 private void tryAcquireImage() {
411 if (canReadImage) {
412 onImageAvailable(reader);
413 }
414 }
415
416 private void onImageReleased(ImageWriter imageWriter) {
417 Log.v(TAG, "Image released");
418
419 if (!canWriteImage) {
420 canWriteImage = true;
421 if (canReadImage) {
422 // Try acquire the image in a handler message, as we may have another call to
423 // onImageAvailable in the thread's message queue.
424 handler.post(this::tryAcquireImage);
425 }
426 }
427 }
428
429 @Override
430 public void repaint() {
431 inner.repaint();
432 }
433
434 @Override
435 public void destroy() {
436 Log.i(TAG, "Destroying ImageSurfaceRenderer");
437 inner.destroy();
438 handler.post(this::destroyReaderWriter);
439 }
440
441 private void destroyReaderWriter() {
442 writer.close();
443 Log.i(TAG, "ImageWriter destroyed");
444 reader.close();
445 Log.i(TAG, "ImageReader destroyed");
446 handlerThread.quitSafely();
447 }
448 }
449}
static final int API_23
Definition: Build.java:13
const Paint & paint
Definition: color_source.cc:38
VkSurfaceKHR surface
Definition: main.cc:49
G_BEGIN_DECLS G_MODULE_EXPORT FlValue * args
uint32_t uint32_t * format
sk_sp< const SkImage > image
Definition: SkRecords.h:269
void Log(const char *format,...) SK_PRINTF_LIKE(1
Definition: TestRunner.cpp:137
CanvasImage Image
Definition: dart_ui.cc:55
SkTileMode TileMode(jint tm)
Definition: Utils.cpp:26
SK_API sk_sp< PrecompileShader > LinearGradient()
int_closure destroy
#define VERSION
Definition: expat_config.h:100