Skip to content

[camera_web] Re: Support for camera stream on web - #7950

Merged
auto-submit[bot] merged 109 commits into
flutter:mainfrom
TecHaxter:camera_web_stream
Nov 6, 2026
Merged

auto-submit[bot] merged 109 commits into
flutter:mainfrom
TecHaxter:camera_web_stream

Conversation

@TecHaxter

@TecHaxter TecHaxter commented Oct 28, 2024 •

Copy link
Copy Markdown
Contributor

Description

Adds image streaming to camera_web, so CameraController.startImageStream works on web. CameraPlugin.supportsImageStreaming() now returns true, and onStreamedFrameAvailable returns a stream of RGBA CameraImageData frames.

How frames are captured

There are two capture paths, chosen per camera when it is created:

Browser support Path Frame source
MediaStreamTrackProcessor available (Chromium-based browsers) MediaStreamTrackProcessor → ReadableStream<VideoFrame> → VideoFrame.copyTo(format: 'RGBA') Every frame the camera track produces (maxBufferSize: 1, so the processor keeps only the newest frame when the consumer falls behind)
Otherwise (e.g. Firefox, Safari) requestAnimationFrame loop → draw the <video> element to an OffscreenCanvas (or a regular canvas if OffscreenCanvas is unavailable) → getImageData Sampled at a 60 fps target

Both paths emit a single-plane RGBA image (bytesPerPixel: 4, bytesPerRow set to the stride the browser reports). format is ImageFormatGroup.unknown with raw: 'rgba8888', because the platform interface has no RGBA group yet (see TODO, flutter/flutter#151193).

Frame ownership

Every emitted frame has its own buffer. The copyTo destination is allocated per frame, and JSUint8Array.toDart returns a view of it, not a second copy. This matches the native implementations: a consumer can keep a CameraImage past the callback, for example across an await or when sending it to an isolate, and later frames won't change its bytes.

An earlier revision reused one buffer across frames. I benchmarked both approaches before switching (Chrome 155 with the fake camera; 640×480 to 1080p at 30, 60 and 90 fps; 3 runs per case):

  • Throughput: identical up to 60 fps. At 1080p90, buffer reuse was +1.3%, but the fake source itself tops out at about 85 fps there.
  • CPU and GC: buffer reuse used about 45–50% less renderer CPU and avoided the GC caused by per-frame allocation. GC pauses with per-frame buffers were at most 4.7 ms, there were no long tasks, and memory did not grow over time.
  • Correctness: with buffer reuse, 100% of frames held for 50 ms or longer had been overwritten by later frames (70/70). With per-frame buffers, 0/70 were.

A buffer pool would need an explicit release API on the platform interface, so I've left it as a possible follow-up.

Lifecycle and errors

  • The frame stream is a broadcast stream. Capture starts when the first listener subscribes and stops when the last one cancels. Camera.dispose() now closes the stream.
  • A VideoFrame is always closed after it is copied, including on errors. The reader lock is released when the loop ends.
  • Frames that can't be copied are skipped, and the stream keeps going (unableToCloneFrame, missingPlaneLayout). The stream closes without an error if the track reader closes (videoTrackReaderClosed).
  • Any other error is emitted on the stream before it closes. In onStreamedFrameAvailable, a CameraWebException is reported as a camera error event and rethrown as a PlatformException.
  • New CameraErrorCodes: noVideoTrack, videoTrackReaderClosed, videoTrackReaderNotInitialized, cameraFrameDimensionsZero, unableToCloneFrame, missingPlaneLayout.

Refactors in CameraService

  • The DOMException → CameraWebException mapping now lives in _domExceptionToCameraWebException, shared by getMediaStreamForOptions and the new frame-stream methods. DOMExceptions are now detected with isA<DOMException>() instead of on DOMException, so the check behaves the same under dart2wasm.
  • New methods so the stream loop can be mocked in tests: hasMediaStreamTrackProcessor, hasPropertyOffScreenCanvas, takeFrame, getMediaStreamVideoTrack, getMediaStreamTrackReader, readVideoTrack, copyVideoFrameToBufferAndGetStride, getCameraImageData.

Known limitations / follow-ups

  • No frame-rate option yet. The fallback path targets 60 fps, and the MediaStreamTrackProcessor path delivers whatever the track produces. CameraImageStreamOptions has no fps field (TODO, [camera] Add framesPerSecond parameter to CameraImageStreamOptions flutter#176148).
  • No RGBA format group in ImageFormatGroup ([camera_android_camerax] Support RGBA_8888 image format group  flutter#151193).
  • Crop-and-scale resolutions aren't scaled on the MediaStreamTrackProcessor path. When Chrome crops and scales the track to fit the requested resolution (e.g. ResolutionPreset.low = 320×240 on a 1080p camera), the VideoFrame's visibleRect is the crop (1440×1080), and only displayWidth/displayHeight are 320×240. Frames are copied at visibleRect size, so they can be larger than previewSize. Scaling to the display size could be a follow-up.

Fixes flutter/flutter#92460

Testing

  • New integration tests, all passing in Chrome (flutter drive -d chrome for each file in example/integration_test/):
    • camera_web_stream_test.dart: supportsImageStreaming, onStreamedFrameAvailable, and the PlatformException mapping.
    • camera_test.dart, in the cameraFrameStream group:
      • emitting frames;
      • closing with an error when reading a frame or building CameraImageData fails;
      • closing quietly when the reader closes;
      • closing on dispose;
      • continuing after unableToCloneFrame;
      • the fallback path's frame-rate target and RGBA byte length;
      • fallback errors.
    • camera_service_test.dart: each new CameraService method, including its error cases.
    • camera_error_code_test.dart: the new error codes.
  • Manually tested with startImageStream in Chrome using a fake camera (--use-file-for-fake-video-capture) at 640×480 to 1080p and 30–90 fps. That covered frame dimensions, stride and byte length, stopping and restarting the stream, background tabs, and dispose.

Pre-Review Checklist

@TecHaxter
TecHaxter requested a review from mdebbar October 20, 2026 18:40

@mdebbar mdebbar 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.

This is looking good! Last round before we merge!

@mdebbar mdebbar added autosubmit Merge PR when tree becomes green via auto submit App CICD Run CI/CD and removed autosubmit Merge PR when tree becomes green via auto submit App labels Oct 21, 2026
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 21, 2026
Comment thread packages/camera/camera_web/CHANGELOG.md Resolved
@mdebbar mdebbar added the CICD Run CI/CD label Oct 28, 2026
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 28, 2026
@TecHaxter

Copy link
Copy Markdown
Contributor Author

Can we re-run CI? Fixed a merged conflict few days back.

@flutter-zl flutter-zl added the CICD Run CI/CD label Nov 4, 2026
@flutter-zl
flutter-zl self-requested a review November 4, 2026 19:22
@TecHaxter

Copy link
Copy Markdown
Contributor Author

I think it is ready to merge. Can anyone add autosubmit label if everything looks sorted?

@flutter-zl flutter-zl 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.

LGTM, thanks for working on this!

I tested it end to end in Chrome with before/after demos:
- Before (camera_web 0.3.5+7): https://flutter-demo-96-before.web.app, where startImageStream throws UnimplementedError
- After (this PR): https://flutter-demo-96-after.web.app, where RGBA frames stream at the camera's rate, and stopping and restarting the stream back to back stays at one loop

@flutter-zl flutter-zl added the autosubmit Merge PR when tree becomes green via auto submit App label Nov 6, 2026
@auto-submit
auto-submit Bot merged commit 3254d60 into flutter:main Nov 6, 2026
14 checks passed
definev pushed a commit to definev/flutter that referenced this pull request Nov 9, 2026
…6210)

flutter/packages@63881f9...565c657

2026-11-08 [email protected] Roll Flutter from
e36b370 to acade0f (1 revision) (flutter/packages#13590)
2026-11-07 [email protected] [material_ui] Fix
DropdownMenu losing text cursor when re-enabled (flutter/packages#13179)
2026-11-07 [email protected] Roll Flutter from
09175cd to e36b370 (22 revisions) (flutter/packages#13586)
2026-11-07 [email protected] [material_ui]
Add Material motion springs (flutter/packages#13338)
2026-11-06 [email protected] [material_ui] Paint
the active RoundedRectSliderTrackShape segment on top in RTL
(flutter/packages#13225)
2026-11-06 [email protected] [pigeon] Replace Experimental with
Preview (flutter/packages#13579)
2026-11-06 [email protected] [material_ui] Fix minimum width
for Material 3 primary tab indicator (flutter/packages#13322)
2026-11-06 [email protected]
[material_ui] Dispose drag-end CurvedAnimation in modal bottom sheet
(flutter/packages#13167)
2026-11-06 [email protected] [camera_web] Re: Support for camera
stream on web (flutter/packages#7950)
2026-11-06 [email protected] Roll Flutter from
3623d2d to 09175cd (61 revisions) (flutter/packages#13578)

If this roll has caused a breakage, revert this CL and set the roller
to dry run mode using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC [email protected] on the revert to ensure that a
human
is aware of the problem.

To file a bug in Flutter:
https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmit Merge PR when tree becomes green via auto submit App CICD Run CI/CD p: camera platform-web triage-web Should be looked at in web triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[camera_web] Support camera image streams on the web

9 participants