[camera_web] Fix TypeError when reading the torch capability (#12647)

## What broke

`setFlashMode` and `takePicture` threw on any camera that has a flash.

The Image Capture specification declares `sequence<boolean> torch` in
`MediaTrackCapabilities`. `package:web` is generated from that specification, so
`torchNullable` was typed `JSArray<JSBoolean>?`.

No browser ships that shape. Chromium and WebKit both declare a bare `boolean torch`.
Reading the property threw before the torch could be applied. The error depends on
the compiler:

| Compiler | Error |
| --- | --- |
| DDC | `TypeError: true: type 'bool' is not a subtype of type 'JSArray<Object?>?'` |
| dart2js | `NoSuchMethodError: method not found: 'gq'` |
| dart2wasm | `ArgumentError: Invalid argument(s)` |

The same line held two more defects:

- `.first` gave the wrong answer for `[false, true]`. A specification-shaped sequence
  lists the values the source accepts, so that means the torch can be turned on.
- `.first` threw `StateError` on an empty sequence.

## The fix

Read `torch` as `JSAny?` and add `canEnableTorch`, which accepts either shape and
returns false for anything else.

A sequence is answered with `any` instead of `first`, and is only trusted when every
element is a boolean. Anything unrecognised raises `torchModeNotSupported` instead of
crashing, and prints a debug-only warning so the shape can be reported.

## Test matrix

| Browser reports | Before | After |
| --- | --- | --- |
| `true` | TypeError | torch on |
| `false` | TypeError | torchModeNotSupported |
| `[false, true]` | wrong answer | torch on |
| `[true]` | torch on | torch on |
| `[false]` | torchModeNotSupported | torchModeNotSupported |
| `[]` | StateError | torchModeNotSupported |
| key absent | torchModeNotSupported | torchModeNotSupported |
| `'yes'` | TypeError | torchModeNotSupported + warning |
| `['yes']` | TypeError | torchModeNotSupported + warning |

Nine cases are covered by integration tests. Six of them fail without this change.

## Devices tested

All nine shapes were run through the real plugin code on each of these. All passed.

| Device | Browser | Result |
| --- | --- | --- |
| Moto G56 5G, Android 16 | Chrome 151 | 9 / 9 |
| macOS 26.3 | Safari 26.3 | 9 / 9 |
| iPhone 17 Pro simulator, iOS 26.1 | Safari | 9 / 9 |
| macOS 26.3 | Chrome 151, dart2js and dart2wasm | 9 / 9 |

On the Moto G56 the rear camera reports `torch: true` as a bare boolean, and the flash
now turns on. The front camera does not report the key at all and raises
`torchModeNotSupported` as expected.

## What was added

- `canEnableTorch` on `NonStandardFieldsOnMediaTrackCapabilities`, which reads either shape.
- A debug-only `debugPrint` warning for a shape that is neither, so an unknown browser can
  be reported rather than failing quietly.
- Nine integration tests covering every shape, plus a check that a recognised shape never warns.
- A CHANGELOG entry and a version bump to `0.3.5+6`.

## What was removed

- The `JSArray<JSBoolean>?` type on `torchNullable`, which no browser matches.
- The `?.toDart.first.toDart ?? false` chain in `_setTorchMode`.

## Issues fixed by this PR

Fixes https://github.com/flutter/flutter/issues/191384

## Pre-Review Checklist

🤖 Generated with [Claude Code](https://claude.com/claude-code)
diff --git a/packages/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md
index a5d9cef..7863809 100644
--- a/packages/camera/camera_web/CHANGELOG.md
+++ b/packages/camera/camera_web/CHANGELOG.md
@@ -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.
diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart
index f03ba19..5027aef 100644
--- a/packages/camera/camera_web/example/integration_test/camera_test.dart
+++ b/packages/camera/camera_web/example/integration_test/camera_test.dart
@@ -12,6 +12,7 @@
 // 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';
@@ -262,7 +263,7 @@
           videoElement = getVideoElementWithBlankStream(const Size(100, 100))..muted = true;
 
           mockVideoTrack.getCapabilities = () {
-            return MediaTrackCapabilities(torch: <JSBoolean>[true.toJS].toJS);
+            return capabilitiesWithTorch(true.toJS);
           }.toJS;
         });
 
@@ -443,6 +444,125 @@
         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 '
@@ -1275,3 +1395,17 @@
     });
   });
 }
+
+/// 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;
+}
diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart
index 242a59d..479e285 100644
--- a/packages/camera/camera_web/lib/src/camera.dart
+++ b/packages/camera/camera_web/lib/src/camera.dart
@@ -333,8 +333,7 @@
 
     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(
diff --git a/packages/camera/camera_web/lib/src/pkg_web_tweaks.dart b/packages/camera/camera_web/lib/src/pkg_web_tweaks.dart
index a279e00..cb79d20 100644
--- a/packages/camera/camera_web/lib/src/pkg_web_tweaks.dart
+++ b/packages/camera/camera_web/lib/src/pkg_web_tweaks.dart
@@ -6,6 +6,7 @@
 
 import 'dart:js_interop';
 
+import 'package:flutter/foundation.dart';
 import 'package:web/web.dart';
 
 /// Adds missing fields to [Element].
@@ -28,8 +29,41 @@
   @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;
diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml
index dadd95c..e274925 100644
--- a/packages/camera/camera_web/pubspec.yaml
+++ b/packages/camera/camera_web/pubspec.yaml
@@ -2,7 +2,7 @@
 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