Skip to content

[camera] Add support for zero-shutter-lag capture - #12374

Open
toyaji wants to merge 7 commits into
flutter:mainfrom
toyaji:camera-zero-shutter-lag
Open

[camera] Add support for zero-shutter-lag capture#12374
toyaji wants to merge 7 commits into
flutter:mainfrom
toyaji:camera-zero-shutter-lag

Conversation

@toyaji

@toyaji toyaji commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Combines the interface and both implementations in one PR per the federated plugin process; splits into an interface-first PR once the direction is approved. The API shape is a proposal for flutter/flutter#190598 — happy to reshape it however you prefer.

Adds explicit zero-shutter-lag control: an opt-in on Android, where the default capture mode leaves it off, and an opt-out on iOS, where the OS enables it automatically when the session supports it.

What's added

camera_platform_interface — two methods on CameraPlatform, modeled on the existing setJpegImageQuality / getSupportedVideoStabilizationModes patterns. Defaults are false / no-op, so nothing changes for platforms or apps that don't opt in:

Future<bool> isZeroShutterLagSupported(int cameraId);
Future<void> setZeroShutterLagEnabled(int cameraId, bool enabled);

camera_android_cameraxCameraInfo.isZslSupported() and ImageCapture.Builder.setCaptureMode(CAPTURE_MODE_ZERO_SHUTTER_LAG). The capture mode is construction-only, so the use case is recreated — the approach setJpegImageQuality already uses for the same constraint, now sharing one helper so the two settings can't drop each other.

camera_avfoundationisZeroShutterLagSupported / isZeroShutterLagEnabled (iOS 17+). Enabling is guarded on the support query (setting it while unsupported throws NSInvalidArgumentException) and wrapped in beginConfiguration/commitConfiguration. The requested state is reapplied after camera switches, which silently reset it. Apps that never call the setter are left untouched, so iOS keeps its automatic behavior on upgrade.

Notes for reviewers

  • Platform defaults differ: Android leaves ZSL off by default, iOS 17+ turns it on automatically when supported — so the same call is an opt-in on Android and an opt-out on iOS. Measurements below.
  • Experimental API: CAPTURE_MODE_ZERO_SHUTTER_LAG is @ExperimentalZeroShutterLag, handled with per-declaration @OptIn like this plugin's existing ExperimentalCamera2Interop / ExperimentalLensFacing / ExperimentalPersistentRecording. gradlew lint is clean.
  • Unsupported cases need no app handling: CameraX falls back on its own; iOS is guarded by the support query.
  • Generated files: regenerating with pigeon 26.1.5 also reverts two pre-existing hand-edits from [camera_android, camera_android_camerax, camera_avfoundation] Add setJpegImageQuality #11616 (a redundant Int32 cast and a missing doc comment).
  • CI: the publish check is red by design — the last commit holds the make-deps-path-based overrides and is reverted before landing.
On-device measurements

Android — Galaxy S21 (SM-G991N), 5 captures per mode, shutter fired via adb shell input tap. A running millisecond stopwatch was photographed; the time visible in each saved photo shows how far behind the shutter tap the captured frame is.

Captured frame Shutter → file saved
MINIMIZE_LATENCY (current) baseline 912 ms
ZERO_SHUTTER_LAG 629 ms earlier 639 ms

iOS — iPhone (iOS 26.5), ResolutionPreset.max, 5 captures per mode after discarding one warm-up capture. Compares AVCapturePhoto.timestamp against the host-clock time of the capturePhoto call, so a negative value means the saved frame predates the call.

Frame vs. shutter call Shutter → delegate
ZSL off +279 ms 941 ms
ZSL on −74 ms 689 ms

With ZSL on, every captured frame predated the capture call. Support is session-dependent rather than device-wide: the same device reports supported at ResolutionPreset.max and unsupported at medium.

Since iOS enables this automatically, these numbers describe what the toggle controls — not an improvement this PR delivers on iOS.

Part of flutter/flutter#190598

Pre-Review Checklist

Footnotes

  1. Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. 2

@google-cla

google-cla Bot commented Aug 5, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@toyaji
toyaji force-pushed the camera-zero-shutter-lag branch from 1041d87 to 5bc45d6 Compare August 5, 2026 10:45
@toyaji toyaji changed the title [camera] Add support for zero-shutter-lag capture (Android) [camera] Add support for zero-shutter-lag capture Aug 5, 2026
@toyaji
toyaji force-pushed the camera-zero-shutter-lag branch from 5bc45d6 to a0bc225 Compare August 5, 2026 12:04
@toyaji
toyaji force-pushed the camera-zero-shutter-lag branch from a0bc225 to ca4ff9c Compare August 5, 2026 23:22
toyaji added 4 commits August 21, 2026 07:25
Adds an opt-in zero-shutter-lag capture API for platforms that support
it. Part of flutter/flutter#190598.

## What

- `isZeroShutterLagSupported(int cameraId)` — support query, defaults to
  `false`
- `setZeroShutterLagEnabled(int cameraId, bool enabled)` — defaults to a
  no-op

## Why non-breaking

Defaults preserve today's behavior on every platform that does not
override them, following the existing `setJpegImageQuality` (no-op
default) and `getSupportedVideoStabilizationModes` (support query)
patterns in this file.
Android implementation of the zero-shutter-lag API added in
camera_platform_interface 2.14.0 (this bumps the constraint to
^2.14.0). Part of flutter/flutter#190598.
Measured on a Galaxy S21 (SM-G991N): the captured frame lands 629 ms
closer to the shutter tap, and shutter-to-file time drops 912 ms ->
639 ms (methodology and raw numbers in the issue).

- `isZeroShutterLagSupported` ->
  [`CameraInfo.isZslSupported()`](https://developer.android.com/reference/androidx/camera/core/CameraInfo#isZslSupported())
- `setZeroShutterLagEnabled` ->
  [`ImageCapture.Builder.setCaptureMode`](https://developer.android.com/reference/androidx/camera/core/ImageCapture.Builder#setCaptureMode(int))
  with
  [`CAPTURE_MODE_ZERO_SHUTTER_LAG`](https://developer.android.com/reference/androidx/camera/core/ImageCapture#CAPTURE_MODE_ZERO_SHUTTER_LAG)

CameraX degrades on its own, and only for still capture. Per the
CAPTURE_MODE_ZERO_SHUTTER_LAG reference above, the mode is disabled
automatically when VideoCapture is bound, flash mode is not OFF, or an
OEM extension is active. The granularity differs: flash falls back per
shot
([`CaptureConfigAdapter.kt`](https://github.com/androidx/androidx/blob/androidx-main/camera/camera-camera2/src/main/java/androidx/camera/camera2/adapter/CaptureConfigAdapter.kt#L133-L138)),
while a device without private reprocessing never activates the ZSL
session at all
([`ZslControl.kt`](https://github.com/androidx/androidx/blob/androidx-main/camera/camera-camera2/src/main/java/androidx/camera/camera2/adapter/ZslControl.kt#L138-L150)).
Apps need no special handling.

<details><summary>Why the ImageCapture use case is recreated</summary>

CameraX only accepts the capture mode at `Builder` time, so
`setZeroShutterLagEnabled` unbinds and recreates the `ImageCapture` use
case — the same approach the existing `setJpegImageQuality` uses for
the same constraint. Both are refactored onto one shared
`_recreateImageCapture()` helper, so both settings (`jpegQuality`, ZSL)
are always reapplied together; `createCamera` applies them on initial
construction as well. Regression tests cover both directions.

</details>

<details><summary>Experimental-API opt-in</summary>

Both CameraX members are annotated
[`@ExperimentalZeroShutterLag`](https://developer.android.com/reference/androidx/camera/core/ExperimentalZeroShutterLag),
a `@RequiresOptIn` marker consumed per declaration. Handled with
`@OptIn`, matching this plugin's existing opt-ins
(`ExperimentalCamera2Interop`, `ExperimentalLensFacing`,
`ExperimentalPersistentRecording`); no build configuration change and
nothing reaches the Dart surface. Verified clean against the repo's
lint setup (`warningsAsErrors` + baseline) with `gradlew lintDebug`.

</details>
iOS implementation of the zero-shutter-lag API added in
camera_platform_interface 2.14.0 (this bumps the constraint to
^2.14.0). Part of flutter/flutter#190598.

## What

- `isZeroShutterLagSupported` ->
  [`AVCapturePhotoOutput.isZeroShutterLagSupported`](https://developer.apple.com/documentation/avfoundation/avcapturephotooutput/iszeroshutterlagsupported)
  (iOS 17+/macOS 14+; `false` on older versions)
- `setZeroShutterLagEnabled` ->
  [`AVCapturePhotoOutput.isZeroShutterLagEnabled`](https://developer.apple.com/documentation/avfoundation/avcapturephotooutput/iszeroshutterlagenabled)

## Why explicit control when iOS auto-enables it

Per the `AVCapturePhotoOutput.h` header, "For apps linked on or after
iOS 17 zero shutter lag is automatically enabled when supported." The
documented role of the setter is the explicit opt-out (WWDC23 session
10105: "you can set AVCapturePhotoOutput.isZeroShutterLagEnabled to
false to opt out" - https://developer.apple.com/videos/play/wwdc2023/10105/),
plus deterministic re-enable: support can be revoked by configuration
changes, and when it is, the enabled state silently reverts.

Because of that auto-enable, iOS 17+ starts with zero shutter lag on
while Android starts with it off, so the same Dart call is an opt-out
here and an opt-in there. No latency numbers are quoted for iOS: the
platform already enables the mode by default, so this change is about
making that state explicit and controllable, not about improving it.

<details><summary>Guard and configuration timing</summary>

The header states the property "may only be set to YES if
zeroShutterLagSupported is YES, otherwise an NSInvalidArgumentException
is thrown", and that changing it "requires a lengthy reconfiguration of
the capture render pipeline", to be done within
`beginConfiguration`/`commitConfiguration` while running. The
implementation guards enabling on the support query and wraps the write
in a configuration block. The requested state is kept (plugin- and
camera-level) and reapplied after session reconfigurations and camera
switches, which silently reset it — mirroring the settings-retention
approach on the Android side. Availability is handled inside the
`CapturePhotoOutput` protocol passthrough (`flutter`-prefixed members,
matching the existing `CaptureDevice.flutterActiveFormat` pattern), so
callers need no availability checks and pre-iOS 17 devices report
unsupported / ignore writes.

</details>

Regenerating with pigeon 26.1.5 also reverts two pre-existing hand-edits
to the generated files from flutter#11616 (a redundant Int32 fallback cast and
a missing Dart doc comment).
Generated with the repo tool per the federated-plugin process
(https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins):

    dart run script/tool/bin/flutter_plugin_tools.dart \
        make-deps-path-based --target-dependencies=camera_platform_interface

CI is expected to be red on the publish check only; that check clears
when this commit is reverted after the platform interface change is
published. Part of flutter/flutter#190598.
@toyaji
toyaji force-pushed the camera-zero-shutter-lag branch from ca4ff9c to e9735d7 Compare August 20, 2026 23:42
@toyaji
toyaji marked this pull request as ready for review August 21, 2026 00:01

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for zero-shutter-lag still image capture to the Flutter camera plugin. It introduces isZeroShutterLagSupported and setZeroShutterLagEnabled to the camera_platform_interface and implements them in the Android (CameraX) and iOS (AVFoundation) implementations, along with corresponding tests and Pigeon updates. Feedback on the changes suggests awaiting the asynchronous isZslSupported call in the Android implementation to avoid potential static analysis errors.

@Piinks
Piinks requested a review from bparrishMines August 25, 2026 20:50
@bparrishMines bparrishMines added the federated: partial_changes PR that contains changes for only a single package of a federated plugin change label Sep 2, 2026

@bparrishMines bparrishMines left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The platform agnostic API looks good to me. This is also missing the API in the camera pacakge.

cc @camsim99 @flutter/ios-reviewers For review of the camera_android_camerax and camera_avfoundation implementations.

Comment on lines +368 to +369
// No-op by default. Platforms that support zero-shutter-lag capture
// override this method.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this has a isZeroShutterLagSupported check, this should throw UnimplementedError by default.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in aa13f31setZeroShutterLagEnabled now throws UnimplementedError by default, matching setImageFileFormat. isZeroShutterLagSupported still returns false by default so it stays safe as the guard.

Comment on lines +354 to +366
/// Enables or disables zero-shutter-lag capture for still image capture.
///
/// When enabled, [takePicture] returns the buffered frame closest to the
/// moment it was called instead of waiting for a new frame to be captured,
/// reducing shutter latency on devices that support it.
///
/// Some platforms enable zero-shutter-lag by default on devices that
/// support it; this setter provides explicit control in either direction.
///
/// This is a best-effort setting; platforms and devices that do not support
/// zero-shutter-lag capture ignore it and fall back to the regular capture
/// pipeline. The default implementation is a no-op so that calling it is
/// always safe.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should mention that isZeroShutterLagSupported should be called first before calling this method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — the dartdoc now leads with "[isZeroShutterLagSupported] must be called first; only call this method on a camera that reports support", and the old "no-op / always safe" wording is gone.

- camera_platform_interface: setZeroShutterLagEnabled default now throws
  UnimplementedError (paired with isZeroShutterLagSupported guard), doc
  notes the guard must be checked first.
- camera: expose CameraController.isZeroShutterLagSupported /
  setZeroShutterLagEnabled with tests; bump to 0.12.0+3 and require
  camera_platform_interface ^2.14.0.
…-lag

# Conflicts:
#	packages/camera/camera_android_camerax/CHANGELOG.md
#	packages/camera/camera_android_camerax/pubspec.yaml
#	packages/camera/camera_avfoundation/CHANGELOG.md
#	packages/camera/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/Messages.swift
Upstream bumped pigeon to ^27.3.2 (camera_avfoundation 0.10.3,
camera_android_camerax 0.7.4+7). Re-run pigeon so the zero-shutter-lag
additions are generated with the same version; bump camera_avfoundation
to 0.10.4 (0.10.3 already taken upstream).
@toyaji

toyaji commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

@bparrishMines addressed all three points:

  • Added CameraController.isZeroShutterLagSupported() / setZeroShutterLagEnabled(bool) to the camera package (aa13f31, bumped to 0.12.0+3).
  • setZeroShutterLagEnabled throws UnimplementedError by default; dartdoc now says the guard must be checked first.
  • Merged main and regenerated pigeon output with v27.3.2 (camera_avfoundation0.10.4). PTAL.

@toyaji
toyaji requested a review from bparrishMines September 3, 2026 05:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants