Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/camera/camera_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.3.5+6

* Fixes a `TypeError` in `setFlashMode` and `takePicture` caused by browsers reporting the
`torch` capability as a `boolean` instead of the `boolean` sequence the Image Capture
specification describes.

## 0.3.5+5

* Removes invalid @JS annotation from extension type constructors.
Expand Down
136 changes: 135 additions & 1 deletion packages/camera/camera_web/example/integration_test/camera_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:camera_platform_interface/camera_platform_interface.dart';
// ignore_for_file: implementation_imports
import 'package:camera_web/src/camera.dart';
import 'package:camera_web/src/types/types.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:mockito/mockito.dart';
Expand Down Expand Up @@ -262,7 +263,7 @@ void main() {
videoElement = getVideoElementWithBlankStream(const Size(100, 100))..muted = true;

mockVideoTrack.getCapabilities = () {
return MediaTrackCapabilities(torch: <JSBoolean>[true.toJS].toJS);
return capabilitiesWithTorch(true.toJS);
}.toJS;
});

Expand Down Expand Up @@ -443,6 +444,125 @@ void main() {
expect(capturedConstraints[0].torch.dartify(), false);
});

// Regression tests for https://github.com/flutter/flutter/issues/191384.
group('reads the torch capability', () {
late List<String> warnings;
late DebugPrintCallback originalDebugPrint;

setUp(() {
warnings = <String>[];
originalDebugPrint = debugPrint;
debugPrint = (String? message, {int? wrapWidth}) {
if (message != null) {
warnings.add(message);
}
};
});

tearDown(() {
debugPrint = originalDebugPrint;
});

final torchCapabilities = <String, (JSAny?, bool)>{
'reported as a bare true by Chromium and WebKit': (true.toJS, true),
'reported as a bare false by Chromium and WebKit': (false.toJS, false),
'reported as a sequence by a browser following the specification': (
<JSBoolean>[false.toJS, true.toJS].toJS,
true,
),
'reported as a sequence holding only true': (<JSBoolean>[true.toJS].toJS, true),
'reported as a sequence holding only false': (<JSBoolean>[false.toJS].toJS, false),
'reported as an empty sequence': (<JSBoolean>[].toJS, false),
'not reported by the browser at all': (null, false),
};

for (final MapEntry<String, (JSAny?, bool)> testCase in torchCapabilities.entries) {
final (JSAny? capability, bool canEnableTorch) = testCase.value;

testWidgets(testCase.key, (WidgetTester tester) async {
mockMediaDevices.getSupportedConstraints = () {
return MediaTrackSupportedConstraints(torch: true);
}.toJS;

mockVideoTrack.getCapabilities = () {
return capabilitiesWithTorch(capability);
}.toJS;

final camera = Camera(textureId: textureId, cameraService: cameraService)
..window = window
..stream = videoStream;

final capturedConstraints = <MediaTrackConstraints>[];
mockVideoTrack.applyConstraints = ([MediaTrackConstraints? constraints]) {
if (constraints != null) {
capturedConstraints.add(constraints);
}
return Future<JSAny?>.value().toJS;
}.toJS;

if (canEnableTorch) {
camera.setFlashMode(FlashMode.torch);

expect(capturedConstraints.length, 1);
expect(capturedConstraints[0].torch.dartify(), true);
} else {
expect(
() => camera.setFlashMode(FlashMode.torch),
throwsA(
isA<CameraWebException>()
.having((CameraWebException e) => e.cameraId, 'cameraId', textureId)
.having(
(CameraWebException e) => e.code,
'code',
CameraErrorCode.torchModeNotSupported,
),
),
);
expect(capturedConstraints, isEmpty);
}

expect(warnings, isEmpty, reason: 'a recognized shape must not warn');
});
}

final unrecognizedCapabilities = <String, JSAny>{
'a value that is not a boolean': 'yes'.toJS,
'a sequence holding something other than booleans': <JSAny>['yes'.toJS].toJS,
};

for (final MapEntry<String, JSAny> testCase in unrecognizedCapabilities.entries) {
testWidgets('warns while debugging when the browser reports '
'${testCase.key}', (WidgetTester tester) async {
mockMediaDevices.getSupportedConstraints = () {
return MediaTrackSupportedConstraints(torch: true);
}.toJS;

mockVideoTrack.getCapabilities = () {
return capabilitiesWithTorch(testCase.value);
}.toJS;

final camera = Camera(textureId: textureId, cameraService: cameraService)
..window = window
..stream = videoStream;

expect(
() => camera.setFlashMode(FlashMode.torch),
throwsA(
isA<CameraWebException>().having(
(CameraWebException e) => e.code,
'code',
CameraErrorCode.torchModeNotSupported,
),
),
);

expect(warnings, hasLength(1));
expect(warnings.single, contains('torch'));
expect(warnings.single, contains('github.com/flutter/flutter/issues'));
});
}
});

group('throws a CameraWebException', () {
testWidgets('with torchModeNotSupported error '
'when the torch mode is not supported '
Expand Down Expand Up @@ -1275,3 +1395,17 @@ void main() {
});
});
}

/// Builds a [MediaTrackCapabilities] reporting [torch] as its torch
/// capability, or omitting the capability entirely when [torch] is null.
///
/// Browser engines disagree on the shape of this value, so it cannot be built
/// with the typed `MediaTrackCapabilities` constructor from `package:web`,
/// which only accepts the `sequence<boolean>` the specification describes.
MediaTrackCapabilities capabilitiesWithTorch(JSAny? torch) {
final capabilities = JSObject();
if (torch != null) {
capabilities.setProperty('torch'.toJS, torch);
}
return capabilities as MediaTrackCapabilities;
}
3 changes: 1 addition & 2 deletions packages/camera/camera_web/lib/src/camera.dart
Original file line number Diff line number Diff line change
Expand Up @@ -333,8 +333,7 @@ class Camera {

if (videoTracks.isNotEmpty) {
final web.MediaStreamTrack defaultVideoTrack = videoTracks.first;
final bool canEnableTorchMode =
defaultVideoTrack.getCapabilities().torchNullable?.toDart.first.toDart ?? false;
final bool canEnableTorchMode = defaultVideoTrack.getCapabilities().canEnableTorch;

if (canEnableTorchMode) {
defaultVideoTrack.applyWebTweakConstraints(
Expand Down
36 changes: 35 additions & 1 deletion packages/camera/camera_web/lib/src/pkg_web_tweaks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import 'dart:js_interop';

import 'package:flutter/foundation.dart';
import 'package:web/web.dart';

/// Adds missing fields to [Element].
Expand All @@ -28,8 +29,41 @@ extension NonStandardFieldsOnMediaTrackCapabilities on MediaTrackCapabilities {
@JS('zoom')
external WebTweakMediaSettingsRange? get zoomNullable;

/// The raw `torch` capability, as reported by the browser.
///
/// Chromium and WebKit report a `boolean`, while the Image Capture
/// specification changed this to `sequence<boolean>` in
/// https://github.com/w3c/mediacapture-image/pull/305. Typed as [JSAny] so
/// that either shape can be read; see [canEnableTorch].
@JS('torch')
external JSArray<JSBoolean>? get torchNullable;
external JSAny? get torchNullable;

/// Whether the camera is able to turn its torch on.
bool get canEnableTorch {
final JSAny? torch = torchNullable;
if (torch == null) {
return false;
}
if (torch.isA<JSBoolean>()) {
return (torch as JSBoolean).toDart;
}
if (torch.isA<JSArray<JSAny?>>()) {
final List<JSAny?> values = (torch as JSArray<JSAny?>).toDart;
if (values.every((JSAny? value) => value.isA<JSBoolean>())) {
return values.any((JSAny? value) => (value! as JSBoolean).toDart);
}
}
assert(() {
debugPrint(
'camera_web: ignoring the `torch` capability of this camera because '
'the browser reported it as neither a boolean nor a sequence of '
'booleans. Please report the browser and its version at '
'https://github.com/flutter/flutter/issues.',
);
return true;
}());
return false;
}

@JS('facingMode')
external JSArray<JSString>? get facingModeNullable;
Expand Down
2 changes: 1 addition & 1 deletion packages/camera/camera_web/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: camera_web
description: A Flutter plugin for getting information about and controlling the camera on Web.
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_web
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
version: 0.3.5+5
version: 0.3.5+6

environment:
sdk: ^3.10.0
Expand Down