diff --git a/packages/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md index a02ae11ed97f..9d870c17ca3f 100644 --- a/packages/camera/camera_web/CHANGELOG.md +++ b/packages/camera/camera_web/CHANGELOG.md @@ -1,5 +1,7 @@ -## NEXT +## 0.3.5+7 +* Fixes `availableCameras` failing when a video input device cannot be started, for example + a virtual camera without a video source. Such devices are now skipped. * Updates minimum supported SDK version to Flutter 3.41/Dart 3.11. ## 0.3.5+6 diff --git a/packages/camera/camera_web/example/integration_test/camera_web_devices_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_devices_test.dart index 3d09f2f1e98f..113149a150c0 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_devices_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_devices_test.dart @@ -380,6 +380,63 @@ void main() { expect(stops.every((bool e) => e), isTrue); }); + testWidgets('skips a video input device ' + 'that cannot be started', (WidgetTester tester) async { + final unreadableVideoDevice = + createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) + as MediaDeviceInfo; + + final videoDevice = + createJSInteropWrapper(FakeMediaDeviceInfo('2', 'Camera 2', MediaDeviceKind.videoInput)) + as MediaDeviceInfo; + + final videoStream = + createJSInteropWrapper( + FakeMediaStream([ + createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, + ]), + ) + as MediaStream; + + mockMediaDevices.enumerateDevices = () { + return Future>.value( + [unreadableVideoDevice, videoDevice].toJS, + ).toJS; + }.toJS; + + when( + cameraService.getMediaStreamForOptions( + CameraOptions(video: VideoConstraints(deviceId: unreadableVideoDevice.deviceId)), + ), + ).thenThrow(CameraWebException(0, CameraErrorCode.notReadable, 'description')); + + when( + cameraService.getMediaStreamForOptions( + CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), + ), + ).thenAnswer((_) => Future.value(videoStream)); + + final List cameras = await CameraPlatform.instance.availableCameras(); + + expect( + cameras, + equals([ + CameraDescription( + name: videoDevice.label, + lensDirection: CameraLensDirection.external, + sensorOrientation: 0, + ), + ]), + ); + + expect( + (CameraPlatform.instance as CameraPlugin).camerasMetadata, + equals({ + cameras.first: CameraMetadata(deviceId: videoDevice.deviceId, facingMode: null), + }), + ); + }); + group('throws CameraException', () { testWidgets('when MediaDevices.enumerateDevices throws DomException', ( WidgetTester tester, @@ -418,6 +475,37 @@ void main() { ); }); + testWidgets('when no video input device can be started', (WidgetTester tester) async { + final videoDevice = + createJSInteropWrapper( + FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput), + ) + as MediaDeviceInfo; + + mockMediaDevices.enumerateDevices = () { + return Future>.value([videoDevice].toJS).toJS; + }.toJS; + + final exception = CameraWebException(0, CameraErrorCode.notReadable, 'description'); + + when( + cameraService.getMediaStreamForOptions( + CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), + ), + ).thenThrow(exception); + + expect( + CameraPlatform.instance.availableCameras(), + throwsA( + isA().having( + (CameraException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + testWidgets('when CameraService.getMediaStreamForOptions ' 'throws PlatformException', (WidgetTester tester) async { final exception = PlatformException( diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index ecb4da1e9fdc..166ccaace4fe 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -119,13 +119,23 @@ class CameraPlugin extends CameraPlatform { /// https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/deviceId#browser_compatibility .where((web.MediaDeviceInfo device) => device.deviceId.isNotEmpty); + // The first error of a video input device that could not be started. + CameraWebException? videoInputDeviceException; + // Map video input devices to camera descriptions. for (final videoInputDevice in videoInputDevices) { // Get the video stream for the current video input device // to later use for the available video tracks. - final web.MediaStream videoStream = await _getVideoStreamForDevice( - videoInputDevice.deviceId, - ); + final web.MediaStream videoStream; + try { + videoStream = await _getVideoStreamForDevice(videoInputDevice.deviceId); + } on CameraWebException catch (e) { + // Skip a video input device that cannot be started, + // e.g. a virtual camera without a video source, + // so that the remaining cameras stay available. + videoInputDeviceException ??= e; + continue; + } // Get all video tracks in the video stream // to later extract the lens direction from the first track. @@ -176,6 +186,12 @@ class CameraPlugin extends CameraPlatform { } } + // Report the error of a video input device that could not be started + // if no camera is available. + if (cameras.isEmpty && videoInputDeviceException != null) { + throw videoInputDeviceException; + } + return cameras; } on web.DOMException catch (e) { throw CameraException(e.name, e.message); diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index e3373765e86a..8ecd22ee0f05 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -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+6 +version: 0.3.5+7 environment: sdk: ^3.11.0