|
6 | 6 |
|
7 | 7 | import android.annotation.TargetApi; |
8 | 8 | import android.graphics.Bitmap; |
| 9 | +import android.graphics.ImageFormat; |
9 | 10 | import android.graphics.Rect; |
10 | 11 | import android.graphics.SurfaceTexture; |
| 12 | +import android.hardware.HardwareBuffer; |
11 | 13 | import android.hardware.SyncFence; |
12 | 14 | import android.media.Image; |
| 15 | +import android.media.ImageReader; |
13 | 16 | import android.os.Build; |
14 | 17 | import android.os.Handler; |
15 | 18 | import android.view.Surface; |
@@ -151,6 +154,22 @@ private void clearDeadListeners() { |
151 | 154 | } |
152 | 155 |
|
153 | 156 | // ------ START TextureRegistry IMPLEMENTATION ----- |
| 157 | + |
| 158 | + /** |
| 159 | + * Creates and returns a new external texture {@link SurfaceProducer} managed by the Flutter |
| 160 | + * engine that is also made available to Flutter code. |
| 161 | + */ |
| 162 | + @Override |
| 163 | + public SurfaceProducer createSurfaceProducer() { |
| 164 | + // TODO(matanl, johnmccutchan): Implement a SurfaceTexture version and switch on whether or |
| 165 | + // not impeller is enabled. |
| 166 | + final ImageReaderSurfaceProducer entry = |
| 167 | + new ImageReaderSurfaceProducer(nextTextureId.getAndIncrement()); |
| 168 | + Log.v(TAG, "New SurfaceProducer ID: " + entry.id()); |
| 169 | + registerImageTexture(isRenderingToImageViewCount, (TextureRegistry.ImageConsumer) entry); |
| 170 | + return entry; |
| 171 | + } |
| 172 | + |
154 | 173 | /** |
155 | 174 | * Creates and returns a new {@link SurfaceTexture} managed by the Flutter engine that is also |
156 | 175 | * made available to Flutter code. |
@@ -182,7 +201,7 @@ public ImageTextureEntry createImageTexture() { |
182 | 201 | final ImageTextureRegistryEntry entry = |
183 | 202 | new ImageTextureRegistryEntry(nextTextureId.getAndIncrement()); |
184 | 203 | Log.v(TAG, "New ImageTextureEntry ID: " + entry.id()); |
185 | | - registerImageTexture(entry.id(), entry); |
| 204 | + registerImageTexture(entry.id(), (TextureRegistry.ImageConsumer) entry); |
186 | 205 | return entry; |
187 | 206 | } |
188 | 207 |
|
@@ -338,7 +357,277 @@ public void run() { |
338 | 357 | } |
339 | 358 |
|
340 | 359 | @Keep |
341 | | - final class ImageTextureRegistryEntry implements TextureRegistry.ImageTextureEntry { |
| 360 | + @TargetApi(29) |
| 361 | + final class ImageReaderSurfaceProducer |
| 362 | + implements TextureRegistry.SurfaceProducer, TextureRegistry.ImageConsumer { |
| 363 | + private static final String TAG = "ImageReaderSurfaceProducer"; |
| 364 | + private static final int MAX_IMAGES = 4; |
| 365 | + |
| 366 | + private final long id; |
| 367 | + |
| 368 | + private boolean released; |
| 369 | + private boolean ignoringFence = false; |
| 370 | + |
| 371 | + private int requestedWidth = 0; |
| 372 | + private int requestedHeight = 0; |
| 373 | + |
| 374 | + private ImageReader activeReader; |
| 375 | + private ImageReader toBeClosedReader; |
| 376 | + private Image latestImage; |
| 377 | + |
| 378 | + private final Handler onImageAvailableHandler = new Handler(); |
| 379 | + private final ImageReader.OnImageAvailableListener onImageAvailableListener = |
| 380 | + new ImageReader.OnImageAvailableListener() { |
| 381 | + @Override |
| 382 | + public void onImageAvailable(ImageReader reader) { |
| 383 | + Image image = null; |
| 384 | + try { |
| 385 | + image = reader.acquireLatestImage(); |
| 386 | + } catch (IllegalStateException e) { |
| 387 | + Log.e(TAG, "onImageAvailable acquireLatestImage failed: " + e.toString()); |
| 388 | + } |
| 389 | + if (image == null) { |
| 390 | + return; |
| 391 | + } |
| 392 | + onImage(image); |
| 393 | + maybeCloseReader(); |
| 394 | + } |
| 395 | + }; |
| 396 | + |
| 397 | + ImageReaderSurfaceProducer(long id) { |
| 398 | + this.id = id; |
| 399 | + } |
| 400 | + |
| 401 | + @Override |
| 402 | + public long id() { |
| 403 | + return id; |
| 404 | + } |
| 405 | + |
| 406 | + @Override |
| 407 | + public void release() { |
| 408 | + if (released) { |
| 409 | + return; |
| 410 | + } |
| 411 | + released = true; |
| 412 | + if (this.latestImage != null) { |
| 413 | + this.latestImage.close(); |
| 414 | + this.latestImage = null; |
| 415 | + } |
| 416 | + if (this.toBeClosedReader != null) { |
| 417 | + this.toBeClosedReader.close(); |
| 418 | + this.toBeClosedReader = null; |
| 419 | + } |
| 420 | + if (this.activeReader != null) { |
| 421 | + this.activeReader.close(); |
| 422 | + this.activeReader = null; |
| 423 | + } |
| 424 | + unregisterTexture(id); |
| 425 | + } |
| 426 | + |
| 427 | + @Override |
| 428 | + public void setSize(int width, int height) { |
| 429 | + if (requestedWidth == width && requestedHeight == height) { |
| 430 | + // No size change. |
| 431 | + return; |
| 432 | + } |
| 433 | + this.requestedHeight = height; |
| 434 | + this.requestedWidth = width; |
| 435 | + // Because the size was changed we will need to close the currently active reader. |
| 436 | + // Instead of closing it eagerly we wait until the a frame is produced at the new |
| 437 | + // size, ensuring that we don't render a blank frame in the app. |
| 438 | + maybeMarkReaderForClose(); |
| 439 | + } |
| 440 | + |
| 441 | + @Override |
| 442 | + public int getWidth() { |
| 443 | + return this.requestedWidth; |
| 444 | + } |
| 445 | + |
| 446 | + @Override |
| 447 | + public int getHeight() { |
| 448 | + return this.requestedHeight; |
| 449 | + } |
| 450 | + |
| 451 | + @Override |
| 452 | + public Surface getSurface() { |
| 453 | + maybeCreateReader(); |
| 454 | + return activeReader.getSurface(); |
| 455 | + } |
| 456 | + |
| 457 | + @Override |
| 458 | + @TargetApi(29) |
| 459 | + public Image acquireLatestImage() { |
| 460 | + Image r; |
| 461 | + synchronized (this) { |
| 462 | + r = this.latestImage; |
| 463 | + this.latestImage = null; |
| 464 | + } |
| 465 | + maybeWaitOnFence(r); |
| 466 | + return r; |
| 467 | + } |
| 468 | + |
| 469 | + private void maybeMarkReaderForClose() { |
| 470 | + synchronized (this) { |
| 471 | + if (this.toBeClosedReader != null) { |
| 472 | + // We only ever have two readers: |
| 473 | + // 1) The reader to be closed after the next image is produced. |
| 474 | + // 2) The reader being used to produce images. |
| 475 | + return; |
| 476 | + } |
| 477 | + this.toBeClosedReader = this.activeReader; |
| 478 | + this.activeReader = null; |
| 479 | + } |
| 480 | + } |
| 481 | + |
| 482 | + private void maybeCloseReader() { |
| 483 | + if (this.toBeClosedReader == null) { |
| 484 | + return; |
| 485 | + } |
| 486 | + this.toBeClosedReader.close(); |
| 487 | + this.toBeClosedReader = null; |
| 488 | + } |
| 489 | + |
| 490 | + private void maybeCreateReader() { |
| 491 | + if (this.activeReader != null) { |
| 492 | + return; |
| 493 | + } |
| 494 | + this.activeReader = createImageReader(); |
| 495 | + } |
| 496 | + |
| 497 | + /** Invoked for each method that is available. */ |
| 498 | + private void onImage(Image image) { |
| 499 | + if (released) { |
| 500 | + return; |
| 501 | + } |
| 502 | + Image toClose; |
| 503 | + synchronized (this) { |
| 504 | + toClose = this.latestImage; |
| 505 | + this.latestImage = image; |
| 506 | + } |
| 507 | + // Close the previously pushed buffer. |
| 508 | + if (toClose != null) { |
| 509 | + Log.e(TAG, "RawSurfaceTexture frame was not acquired in a timely manner."); |
| 510 | + toClose.close(); |
| 511 | + } |
| 512 | + if (image != null) { |
| 513 | + // Mark that we have a new frame available. Eventually the raster thread will |
| 514 | + // call acquireLatestImage. |
| 515 | + markTextureFrameAvailable(id); |
| 516 | + } |
| 517 | + } |
| 518 | + |
| 519 | + @TargetApi(33) |
| 520 | + private void waitOnFence(Image image) { |
| 521 | + try { |
| 522 | + SyncFence fence = image.getFence(); |
| 523 | + boolean signaled = fence.awaitForever(); |
| 524 | + if (!signaled) { |
| 525 | + Log.e(TAG, "acquireLatestImage image's fence was never signalled."); |
| 526 | + } |
| 527 | + } catch (IOException e) { |
| 528 | + // Drop. |
| 529 | + } |
| 530 | + } |
| 531 | + |
| 532 | + private void maybeWaitOnFence(Image image) { |
| 533 | + if (image == null) { |
| 534 | + return; |
| 535 | + } |
| 536 | + if (ignoringFence) { |
| 537 | + return; |
| 538 | + } |
| 539 | + if (Build.VERSION.SDK_INT >= 33) { |
| 540 | + // The fence API is only available on Android >= 33. |
| 541 | + waitOnFence(image); |
| 542 | + return; |
| 543 | + } |
| 544 | + if (!ignoringFence) { |
| 545 | + // Log once per ImageTextureEntry. |
| 546 | + ignoringFence = true; |
| 547 | + Log.w(TAG, "ImageTextureEntry can't wait on the fence on Android < 33"); |
| 548 | + } |
| 549 | + } |
| 550 | + |
| 551 | + @Override |
| 552 | + protected void finalize() throws Throwable { |
| 553 | + try { |
| 554 | + if (released) { |
| 555 | + return; |
| 556 | + } |
| 557 | + if (latestImage != null) { |
| 558 | + // Be sure to finalize any cached image. |
| 559 | + latestImage.close(); |
| 560 | + latestImage = null; |
| 561 | + } |
| 562 | + if (this.toBeClosedReader != null) { |
| 563 | + this.toBeClosedReader.close(); |
| 564 | + } |
| 565 | + if (this.activeReader != null) { |
| 566 | + this.activeReader.close(); |
| 567 | + } |
| 568 | + released = true; |
| 569 | + handler.post(new TextureFinalizerRunnable(id, flutterJNI)); |
| 570 | + } finally { |
| 571 | + super.finalize(); |
| 572 | + } |
| 573 | + } |
| 574 | + |
| 575 | + @TargetApi(33) |
| 576 | + private ImageReader createImageReader33() { |
| 577 | + final ImageReader.Builder builder = new ImageReader.Builder(requestedWidth, requestedHeight); |
| 578 | + // Allow for double buffering. |
| 579 | + builder.setMaxImages(MAX_IMAGES); |
| 580 | + // Use PRIVATE image format so that we can support video decoding. |
| 581 | + // TODO(johnmccutchan): Should we always use PRIVATE here? It may impact our |
| 582 | + // ability to read back texture data. If we don't always want to use it, how do |
| 583 | + // we |
| 584 | + // decide when to use it or not? Perhaps PlatformViews can indicate if they may |
| 585 | + // contain |
| 586 | + // DRM'd content. |
| 587 | + // I need to investigate how PRIVATE impacts our ability to take screenshots or |
| 588 | + // capture |
| 589 | + // the output of Flutter application. |
| 590 | + builder.setImageFormat(ImageFormat.PRIVATE); |
| 591 | + // Hint that consumed images will only be read by GPU. |
| 592 | + builder.setUsage(HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE); |
| 593 | + final ImageReader reader = builder.build(); |
| 594 | + reader.setOnImageAvailableListener(this.onImageAvailableListener, onImageAvailableHandler); |
| 595 | + return reader; |
| 596 | + } |
| 597 | + |
| 598 | + @TargetApi(29) |
| 599 | + private ImageReader createImageReader29() { |
| 600 | + final ImageReader reader = |
| 601 | + ImageReader.newInstance( |
| 602 | + requestedWidth, |
| 603 | + requestedHeight, |
| 604 | + ImageFormat.PRIVATE, |
| 605 | + MAX_IMAGES, |
| 606 | + HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE); |
| 607 | + reader.setOnImageAvailableListener(this.onImageAvailableListener, onImageAvailableHandler); |
| 608 | + return reader; |
| 609 | + } |
| 610 | + |
| 611 | + private ImageReader createImageReader() { |
| 612 | + if (Build.VERSION.SDK_INT >= 33) { |
| 613 | + return createImageReader33(); |
| 614 | + } else if (Build.VERSION.SDK_INT >= 29) { |
| 615 | + return createImageReader29(); |
| 616 | + } |
| 617 | + throw new UnsupportedOperationException( |
| 618 | + "ImageReaderPlatformViewRenderTarget requires API version 29+"); |
| 619 | + } |
| 620 | + |
| 621 | + @VisibleForTesting |
| 622 | + public void disableFenceForTest() { |
| 623 | + // Roboelectric's implementation of SyncFence is borked. |
| 624 | + ignoringFence = true; |
| 625 | + } |
| 626 | + } |
| 627 | + |
| 628 | + @Keep |
| 629 | + final class ImageTextureRegistryEntry |
| 630 | + implements TextureRegistry.ImageTextureEntry, TextureRegistry.ImageConsumer { |
342 | 631 | private static final String TAG = "ImageTextureRegistryEntry"; |
343 | 632 | private final long id; |
344 | 633 | private boolean released; |
@@ -408,6 +697,9 @@ private void maybeWaitOnFence(Image image) { |
408 | 697 | if (image == null) { |
409 | 698 | return; |
410 | 699 | } |
| 700 | + if (ignoringFence) { |
| 701 | + return; |
| 702 | + } |
411 | 703 | if (Build.VERSION.SDK_INT >= 33) { |
412 | 704 | // The fence API is only available on Android >= 33. |
413 | 705 | waitOnFence(image); |
@@ -472,7 +764,8 @@ protected void finalize() throws Throwable { |
472 | 764 | public void startRenderingToSurface(@NonNull Surface surface, boolean onlySwap) { |
473 | 765 | if (!onlySwap) { |
474 | 766 | // Stop rendering to the surface releases the associated native resources, which |
475 | | - // causes a glitch when toggling between rendering to an image view (hybrid composition) and |
| 767 | + // causes a glitch when toggling between rendering to an image view (hybrid |
| 768 | + // composition) and |
476 | 769 | // rendering directly to a Surface or Texture view. For more, |
477 | 770 | // https://github.com/flutter/flutter/issues/95343 |
478 | 771 | stopRenderingToSurface(); |
@@ -645,8 +938,8 @@ private void registerTexture(long textureId, @NonNull SurfaceTextureWrapper text |
645 | 938 | } |
646 | 939 |
|
647 | 940 | private void registerImageTexture( |
648 | | - long textureId, @NonNull TextureRegistry.ImageTextureEntry textureEntry) { |
649 | | - flutterJNI.registerImageTexture(textureId, textureEntry); |
| 941 | + long textureId, @NonNull TextureRegistry.ImageConsumer imageTexture) { |
| 942 | + flutterJNI.registerImageTexture(textureId, imageTexture); |
650 | 943 | } |
651 | 944 |
|
652 | 945 | // TODO(mattcarroll): describe the native behavior that this invokes |
|
0 commit comments