Skip to content

Commit

Permalink
Migrate camera/android from SurfaceTexture->SurfaceProducer. (f…
Browse files Browse the repository at this point in the history
…lutter#6461)

_**WIP**: We do not plan to land this PR until the next stable release (>= April 3rd 2024)_.

Work towards flutter/flutter#145930.

## Details

Migrates uses of `createSurfaceTexture` to `createSurfaceProducer`, which is intended to have no change in behavior, but _does_ change the backend rendering path, so it will require more testing (and we're also open to minor API renames or changes before it becomes stable).

## Background 

Android plugins previously requested a `SurfaceTexture` from the Android embedder, and used that to produce a `Surface` to render external textures on (i.e. `video_player`).  This worked because 100% of Flutter applications on Android used OpenGLES (via our Skia backend), and `SurfaceTexture` is actually an (opaque) OpenGLES-texture.

Starting soon (roughly ~Q3, this is not a guarantee and just an estimate), Flutter on Android will start to use our new Impeller graphics backend, which on newer devices (`>= API_VERSION_28`), will default to the Vulkan, _not_ OpenGLES. In other words, `SurfaceTexture` will cease to work (it is possible, but non-trivial, to map an OpenGLES texture over to Vulkan).

After consultation with the Android team, they helped us understand that vending `SurfaceTexture` (the _consumer-side_ API) was never the right abstraction, and we should have been vending the _producer-side_ API, or `Surface` directly. The new `SurfaceProducer` API is exactly that - it generates a `Surface`, and similar to our platform view strategy, picks the "right" _consumer-side_ implementation details _for_ the user/plugin packages.

The new `SurfaceProducer` API has 2 possible rendering types (as an implementation detail):

- `SurfaceTexture`, for older OpenGLES devices, which works exactly as it does today.
- `ImageReader`, for newer OpenGLES _or_ Vulkan devices.

These are some subtle nuances in how these two APIs work differently (one example: flutter/flutter#144407), but our theory at this point is we don't expect these changes to be observed by any users, and we have other ideas if necessary. 

> [!NOTE]
> These invariants are [tested on CI in `flutter/engine`](https://github.com/flutter/engine/tree/main/testing/scenario_app/android#ci-configuration).

Points of contact: 
- @matanlurey or @jonahwilliams  (Flutter Engine)
- @johnmccutchan or @reidbaker  (Flutter on Android)
  • Loading branch information
matanlurey authored May 29, 2024
1 parent e35f291 commit 058851b
Show file tree
Hide file tree
Showing 6 changed files with 47 additions and 79 deletions.
6 changes: 5 additions & 1 deletion packages/camera/camera_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.10.9+4

* [Supports Impeller](https://docs.flutter.dev/release/breaking-changes/android-surface-plugins).

## 0.10.9+3

* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4.
Expand Down Expand Up @@ -36,7 +40,7 @@

## 0.10.8+14

* Fixes `pausePreview` null pointer error. `pausePreview` should not be called
* Fixes `pausePreview` null pointer error. `pausePreview` should not be called
when camera is closed or not configured.
* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import android.app.Activity;
import android.content.Context;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraDevice;
Expand Down Expand Up @@ -63,7 +62,7 @@
import io.flutter.plugins.camera.media.MediaRecorderBuilder;
import io.flutter.plugins.camera.types.CameraCaptureProperties;
import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper;
import io.flutter.view.TextureRegistry.SurfaceTextureEntry;
import io.flutter.view.TextureRegistry;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
Expand Down Expand Up @@ -113,7 +112,7 @@ class Camera
*/
@VisibleForTesting int initialCameraFacing;

@VisibleForTesting final SurfaceTextureEntry flutterTexture;
@VisibleForTesting final TextureRegistry.SurfaceProducer surfaceProducer;
private final VideoCaptureSettings videoCaptureSettings;
private final Context applicationContext;
final DartMessenger dartMessenger;
Expand Down Expand Up @@ -214,17 +213,16 @@ public VideoCaptureSettings(@NonNull ResolutionPreset resolutionPreset, boolean

public Camera(
final Activity activity,
final SurfaceTextureEntry flutterTexture,
final TextureRegistry.SurfaceProducer surfaceProducer,
final CameraFeatureFactory cameraFeatureFactory,
final DartMessenger dartMessenger,
final CameraProperties cameraProperties,
final VideoCaptureSettings videoCaptureSettings) {

if (activity == null) {
throw new IllegalStateException("No activity available!");
}
this.activity = activity;
this.flutterTexture = flutterTexture;
this.surfaceProducer = surfaceProducer;
this.dartMessenger = dartMessenger;
this.applicationContext = activity.getApplicationContext();
this.cameraProperties = cameraProperties;
Expand All @@ -243,7 +241,6 @@ public Camera(
if (videoCaptureSettings.fps != null && videoCaptureSettings.fps.intValue() > 0) {
recordingFps = videoCaptureSettings.fps;
} else {

if (SdkCapabilityChecker.supportsEncoderProfiles()) {
EncoderProfiles encoderProfiles = getRecordingProfile();
if (encoderProfiles != null && encoderProfiles.getVideoProfiles().size() > 0) {
Expand All @@ -256,7 +253,6 @@ public Camera(
}

if (recordingFps != null && recordingFps.intValue() > 0) {

final FpsRangeFeature fpsRange = new FpsRangeFeature(cameraProperties);
fpsRange.setValue(new Range<Integer>(recordingFps, recordingFps));
this.cameraFeatures.setFpsRange(fpsRange);
Expand Down Expand Up @@ -307,8 +303,9 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException {

MediaRecorderBuilder mediaRecorderBuilder;

// TODO(camsim99): Revert changes that allow legacy code to be used when recordingProfile is null
// once this has largely been fixed on the Android side. https://github.com/flutter/flutter/issues/119668
// TODO(camsim99): Revert changes that allow legacy code to be used when recordingProfile is
// null once this has largely been fixed on the Android side.
// https://github.com/flutter/flutter/issues/119668
if (SdkCapabilityChecker.supportsEncoderProfiles() && getRecordingProfile() != null) {
mediaRecorderBuilder =
new MediaRecorderBuilder(
Expand Down Expand Up @@ -386,7 +383,8 @@ public void onOpened(@NonNull CameraDevice device) {
cameraDevice = new DefaultCameraDeviceWrapper(device);
try {
startPreview();
if (!recordingVideo) { // only send initialization if we werent already recording and switching cameras
if (!recordingVideo) { // only send initialization if we werent already recording and
// switching cameras
dartMessenger.sendCameraInitializedEvent(
resolutionFeature.getPreviewSize().getWidth(),
resolutionFeature.getPreviewSize().getHeight(),
Expand Down Expand Up @@ -470,11 +468,10 @@ private void createCaptureSession(

// Build Flutter surface to render to.
ResolutionFeature resolutionFeature = cameraFeatures.getResolution();
SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture();
surfaceTexture.setDefaultBufferSize(
surfaceProducer.setSize(
resolutionFeature.getPreviewSize().getWidth(),
resolutionFeature.getPreviewSize().getHeight());
Surface flutterSurface = new Surface(surfaceTexture);
Surface flutterSurface = surfaceProducer.getSurface();
previewRequestBuilder.addTarget(flutterSurface);

List<Surface> remainingSurfaces = Arrays.asList(surfaces);
Expand Down Expand Up @@ -1160,7 +1157,8 @@ public void resumePreview() {
}

public void startPreview() throws CameraAccessException, InterruptedException {
// If recording is already in progress, the camera is being flipped, so send it through the VideoRenderer to keep the correct orientation.
// If recording is already in progress, the camera is being flipped, so send it through the
// VideoRenderer to keep the correct orientation.
if (recordingVideo) {
startPreviewWithVideoRendererStream();
} else {
Expand Down Expand Up @@ -1193,7 +1191,6 @@ private void startPreviewWithVideoRendererStream()
}

if (cameraProperties.getLensFacing() != initialCameraFacing) {

// If the new camera is facing the opposite way than the initial recording,
// the rotation should be flipped 180 degrees.
rotation = (rotation + 180) % 360;
Expand Down Expand Up @@ -1361,13 +1358,13 @@ public void uncaughtException(Thread thread, Throwable ex) {

public void setDescriptionWhileRecording(
@NonNull final Result result, CameraProperties properties) {

if (!recordingVideo) {
result.error("setDescriptionWhileRecordingFailed", "Device was not recording", null);
return;
}

// See VideoRenderer.java; support for this EGL extension is required to switch camera while recording.
// See VideoRenderer.java; support for this EGL extension is required to switch camera while
// recording.
if (!SdkCapabilityChecker.supportsEglRecordableAndroid()) {
result.error(
"setDescriptionWhileRecordingFailed",
Expand Down Expand Up @@ -1400,7 +1397,7 @@ public void dispose() {
Log.i(TAG, "dispose");

close();
flutterTexture.release();
surfaceProducer.release();
getDeviceOrientationManager().stop();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,27 +393,25 @@ private void instantiateCamera(MethodCall call, Result result) throws CameraAcce
Integer videoBitrate = call.argument("videoBitrate");
Integer audioBitrate = call.argument("audioBitrate");

TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture =
textureRegistry.createSurfaceTexture();
TextureRegistry.SurfaceProducer surfaceProducer = textureRegistry.createSurfaceProducer();
DartMessenger dartMessenger =
new DartMessenger(
messenger, flutterSurfaceTexture.id(), new Handler(Looper.getMainLooper()));
new DartMessenger(messenger, surfaceProducer.id(), new Handler(Looper.getMainLooper()));
CameraProperties cameraProperties =
new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity));
ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset);

camera =
new Camera(
activity,
flutterSurfaceTexture,
surfaceProducer,
new CameraFeatureFactoryImpl(),
dartMessenger,
cameraProperties,
new Camera.VideoCaptureSettings(
resolutionPreset, enableAudio, fps, videoBitrate, audioBitrate));

Map<String, Object> reply = new HashMap<>();
reply.put("cameraId", flutterSurfaceTexture.id());
reply.put("cameraId", surfaceProducer.id());
result.success(reply);
}

Expand Down
Loading

0 comments on commit 058851b

Please sign in to comment.