Skip to content

Conversation

@TecHaxter
Copy link

This PR aims to provide support for strartImageStream and stopImageStream on Web.

#92460

Based on #6944 from the archived plugins repository

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] wiki page, which explains my responsibilities.
  • I read and followed the [relevant style guides] and ran the auto-formatter. (Unlike the flutter/flutter repo, the flutter/packages repo does use dart format.)
  • I signed the [CLA].
  • The title of the PR starts with the name of the package surrounded by square brackets, e.g. [shared_preferences]
  • I [linked to at least one issue that this PR fixes] in the description above.
  • I updated pubspec.yaml with an appropriate new version according to the [pub versioning philosophy], or this PR is [exempt from version changes].
  • I updated CHANGELOG.md to add a description of the change, [following repository CHANGELOG style].
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is [test-exempt].
  • All existing and new tests are passing.

POV: my first PR on a public repo
Contains required commits from the PR #6443, resolves unnecessary 202 commits

@TecHaxter TecHaxter requested a review from ditman November 6, 2024 19:10
@TecHaxter
Copy link
Author

Hi @ditman The PR is ready for another review, Thanks :)

@stuartmorgan-g
Copy link
Collaborator

From triage: Ping @ditman on this re-review.

@stuartmorgan-g stuartmorgan-g added the triage-web Should be looked at in web triage label Jan 14, 2025
@stuartmorgan-g
Copy link
Collaborator

Update from triage: we're working through trying to find a web reviewer with availability to look at this. Apologies for the delay.

@Piinks
Copy link
Contributor

Piinks commented Jun 4, 2025

@yjbanov can you suggest a reviewer for this change? It has been blocked on a web review.

@mdebbar mdebbar self-requested a review June 11, 2025 18:23
@mdebbar
Copy link
Contributor

mdebbar commented Aug 6, 2025

/gemini review

Copy link

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

Choose a reason for hiding this comment

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

Code Review

This PR introduces support for image streams on the web for the camera plugin. The changes are well-structured, with new functionality added to Camera and CameraService classes, and corresponding tests.

My review has identified a few areas for improvement:

  • A critical issue in CameraService where a null-check operator could lead to a runtime error.
  • Some maintainability issues in Camera related to a C-style variable declaration and a magic number.
  • Opportunities to reduce code duplication in the new integration tests.

Overall, this is a great contribution. Addressing these points will improve the robustness and maintainability of the code.

required int fps,
}) {
int? animationFrameId;
num then = 0, fpsInterval = 1000 / fps;

Choose a reason for hiding this comment

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

medium

It's preferred to declare each variable on its own line. This C-style declaration can be less readable and is not idiomatic Dart.

    num then = 0;
    num fpsInterval = 1000 / fps;

Copy link
Contributor

Choose a reason for hiding this comment

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

and let's give then a more descriptive name like lastFrameTimestamp.

Copy link
Author

Choose a reason for hiding this comment

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

This is addressed.

final num elapsed = timestamp - then;

// if we're close to the next frame (by ~8ms), do it.
if (fpsInterval - elapsed <= 8) {

Choose a reason for hiding this comment

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

medium

The number 8 here is a magic number. While the comment explains its purpose, it would be clearer to define it as a named constant (e.g., _frameTimeToleranceMs). This improves readability and maintainability, as the value's purpose is self-documenting and it can be easily changed in one place.

Copy link
Author

Choose a reason for hiding this comment

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

This is addressed.

Comment on lines 914 to 942
testWidgets(
'returns true if broswer has OffscreenCanvas '
'otherwise false',
(WidgetTester widgetTester) async {
when(
() => jsUtil.hasProperty(
window,
'OffscreenCanvas'.toJS,
),
).thenReturn(true);
final bool hasOffScreenCanvas =
cameraService.hasPropertyOffScreenCanvas();
expect(
hasOffScreenCanvas,
true,
);
when(
() => jsUtil.hasProperty(
window,
'OffscreenCanvas'.toJS,
),
).thenReturn(false);
final bool hasNotOffScreenCanvas =
cameraService.hasPropertyOffScreenCanvas();
expect(
hasNotOffScreenCanvas,
false,
);
},

Choose a reason for hiding this comment

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

medium

This test and the subsequent test on lines 944-1003 are nearly identical, with the only difference being the mocked return value of hasPropertyOffScreenCanvas. This duplication can be reduced by creating a shared test implementation or using a for loop to run the test for both true and false cases. This will make the tests more maintainable.

Copy link
Author

Choose a reason for hiding this comment

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

This is addressed now.

Comment on lines 1713 to 1761
testWidgets(
'CameraImageData bytes is a multiple of 4 '
'when browser supports OffscreenCanvas',
(WidgetTester tester) async {
final VideoElement videoElement = getVideoElementWithBlankStream(
const Size(10, 10),
);

final Camera camera = Camera(
textureId: textureId,
cameraService: cameraService,
)..videoElement = videoElement;

when(() => cameraService.hasPropertyOffScreenCanvas()).thenReturn(
true,
);

when(
() => cameraService.takeFrame(videoElement),
).thenAnswer(
(_) => CameraImageData(
format: const CameraImageFormat(
ImageFormatGroup.unknown,
raw: 0,
),
planes: <CameraImagePlane>[
CameraImagePlane(
bytes: Uint8List(32),
bytesPerRow: videoElement.width * 4,
),
],
height: 10,
width: 10,
),
);

final CameraImageData cameraImageData =
await camera.cameraFrameStream().first;
expect(
cameraImageData,
equals(
isA<CameraImageData>().having(
(CameraImageData e) => e.planes.first.bytes.length % 4,
'bytes',
equals(0),
),
),
);
},

Choose a reason for hiding this comment

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

medium

This test and the subsequent test on lines 1763-1812 are almost identical, differing only in the mocked value for hasPropertyOffScreenCanvas. To improve maintainability and reduce code duplication, consider refactoring them into a single test function that is parameterized with the boolean for OffscreenCanvas support, or use a loop.

Copy link
Author

Choose a reason for hiding this comment

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

This has been addressed.

Comment on lines 650 to 651
// TODO(replace): introduced fps in
/// [CameraImageStreamOptions]

Choose a reason for hiding this comment

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

medium

This TODO should be addressed, and the fps should be introduced in [CameraImageStreamOptions].

Copy link
Author

Choose a reason for hiding this comment

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

I don't think this TODO needs to be addressed in this PR, because PR checks would fail if it would contain changes in more than one package.

@stuartmorgan-g
Copy link
Collaborator

From triage: @mdebbar What's the status of the web review here?

Copy link
Contributor

@mdebbar mdebbar left a comment

Choose a reason for hiding this comment

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

This looks really close to being ready to land. I just some minor polishing comments.

## NEXT
## 0.3.6

* Supporting camera image stream on web.
Copy link
Contributor

Choose a reason for hiding this comment

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

To keep this consistent with the rest of the file:

Suggested change
* Supporting camera image stream on web.
* Adds support for camera image stream on web.

Copy link
Author

Choose a reason for hiding this comment

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

This is addressed.

Comment on lines 918 to 941
when(
() => jsUtil.hasProperty(
window,
'OffscreenCanvas'.toJS,
),
).thenReturn(true);
final bool hasOffScreenCanvas =
cameraService.hasPropertyOffScreenCanvas();
expect(
hasOffScreenCanvas,
true,
);
when(
() => jsUtil.hasProperty(
window,
'OffscreenCanvas'.toJS,
),
).thenReturn(false);
final bool hasNotOffScreenCanvas =
cameraService.hasPropertyOffScreenCanvas();
expect(
hasNotOffScreenCanvas,
false,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can use a loop to simplify:

Suggested change
when(
() => jsUtil.hasProperty(
window,
'OffscreenCanvas'.toJS,
),
).thenReturn(true);
final bool hasOffScreenCanvas =
cameraService.hasPropertyOffScreenCanvas();
expect(
hasOffScreenCanvas,
true,
);
when(
() => jsUtil.hasProperty(
window,
'OffscreenCanvas'.toJS,
),
).thenReturn(false);
final bool hasNotOffScreenCanvas =
cameraService.hasPropertyOffScreenCanvas();
expect(
hasNotOffScreenCanvas,
false,
);
for (final supportsOffscreenCanvas in [true, false]) {
when(
() => jsUtil.hasProperty(
window,
'OffscreenCanvas'.toJS,
),
).thenReturn(value);
final bool hasOffScreenCanvas =
cameraService.hasPropertyOffScreenCanvas();
expect(
cameraService.hasPropertyOffScreenCanvas(),
supportsOffscreenCanvas ? isTrue : isFalse,
);
}

Copy link
Author

Choose a reason for hiding this comment

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

Just so you know – this has been addressed now.

Comment on lines 944 to 948
testWidgets(
'returns Camera Image of Size '
'when videoElement is of Size '
'when browser supports OffscreenCanvas',
(WidgetTester widgetTester) async {
Copy link
Contributor

Choose a reason for hiding this comment

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

This test and the one below are basically the same, the only difference is the boolean value of the hasPropertyOffScreenCanvas mock.

Combine both into a single test:

      testWidgets(
        'returns Camera Image of Size '
        'when videoElement is of Size '
        'regardless of OffscreenCanvas support',
        (WidgetTester widgetTester) async {
          for (final supportsOffscreenCanvas in [true, false]) {
            ...
          }
        }

Copy link
Author

Choose a reason for hiding this comment

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

This has been resolved now.

Comment on lines 1713 to 1716
testWidgets(
'CameraImageData bytes is a multiple of 4 '
'when browser supports OffscreenCanvas',
(WidgetTester tester) async {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can be combined with the test below, similar to my previous comment.

Copy link
Author

Choose a reason for hiding this comment

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

I've taken care of this.


/// Used to check if allowed to paint canvas off screen
@visibleForTesting
bool canUseOffscreenCanvas = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this never changes, so let's make it final:

Suggested change
bool canUseOffscreenCanvas = false;
final bool canUseOffscreenCanvas;

Copy link
Author

Choose a reason for hiding this comment

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

This is addressed now.

Comment on lines 650 to 651
// TODO(replace): introduced fps in
/// [CameraImageStreamOptions]
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Author

@TecHaxter TecHaxter Sep 27, 2025

Choose a reason for hiding this comment

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

I have updated the TODO comment. But haven't created an issue for that. Should I create one?

Update: I've created an issue and included that in the comment.

required int fps,
}) {
int? animationFrameId;
num then = 0, fpsInterval = 1000 / fps;
Copy link
Contributor

Choose a reason for hiding this comment

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

and let's give then a more descriptive name like lastFrameTimestamp.

@TecHaxter TecHaxter requested a review from mdebbar September 28, 2025 11:06
@stuartmorgan-g
Copy link
Collaborator

From triage: @mdebbar This looks to be ready for re-review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

5 participants