From b751fd981aff7355580f3a0f9192a8255c52da53 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Tue, 13 Jul 2021 16:20:56 +0200 Subject: [PATCH 1/7] feat: add camera with options to play, stop and take a picture --- .../camera/camera_web/lib/src/camera.dart | 192 ++++++++++++++++++ .../camera_web/lib/src/shims/dart_ui.dart | 10 + .../lib/src/shims/dart_ui_fake.dart | 28 +++ .../lib/src/shims/dart_ui_real.dart | 5 + .../lib/src/types/camera_error_codes.dart | 25 +++ 5 files changed, 260 insertions(+) create mode 100644 packages/camera/camera_web/lib/src/camera.dart create mode 100644 packages/camera/camera_web/lib/src/shims/dart_ui.dart create mode 100644 packages/camera/camera_web/lib/src/shims/dart_ui_fake.dart create mode 100644 packages/camera/camera_web/lib/src/shims/dart_ui_real.dart create mode 100644 packages/camera/camera_web/lib/src/types/camera_error_codes.dart diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart new file mode 100644 index 000000000000..e4d8023d5db3 --- /dev/null +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -0,0 +1,192 @@ +import 'dart:html' as html; +import 'shims/dart_ui.dart' as ui; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/types/camera_error_codes.dart'; +import 'package:camera_web/src/types/camera_options.dart'; + +String _getViewType(int cameraId) => 'plugins.flutter.io/camera_$cameraId'; + +/// A camera initialized from the media devices in the current [window]. +/// The obtained camera is constrained by the [options] used when +/// querying the media input in [_getMediaStream]. +/// +/// The camera stream is displayed in the [videoElement] wrapped in the +/// [divElement] to avoid overriding the custom styles applied to +/// the video element in [_applyDefaultVideoStyles]. +/// See: https://github.com/flutter/flutter/issues/79519 +/// +/// The camera can be played/stopped by calling [play]/[stop] +/// or may capture a picture by [takePicture]. +/// +/// The [textureId] is used to register a camera view with the id +/// returned by [_getViewType]. +class Camera { + /// Creates a new instance of [Camera] + /// with the given [textureId] and optional + /// [options] and [window]. + Camera({ + required this.textureId, + this.options = const CameraOptions(), + html.Window? window, + }) : window = window ?? html.window; + + /// The texture id used to register the camera view. + final int textureId; + + /// The camera options used to initialize a camera, empty by default. + final CameraOptions options; + + /// The current browser window used to access device cameras. + final html.Window window; + + /// The video element that displays the camera stream. + /// Initialized in [initialize]. + late html.VideoElement videoElement; + + /// The wrapping element for the [videoElement] to avoid overriding + /// the custom styles applied in [_applyDefaultVideoStyles]. + /// Initialized in [initialize]. + late html.DivElement divElement; + + /// Initializes the camera stream displayed in the [videoElement]. + /// Registers the camera view with [textureId] under [_getViewType] type. + Future initialize() async { + final isSupported = window.navigator.mediaDevices?.getUserMedia != null; + if (!isSupported) { + throw CameraException( + CameraErrorCodes.notSupported, + 'The camera is not supported on this device.', + ); + } + + videoElement = html.VideoElement(); + _applyDefaultVideoStyles(videoElement); + + divElement = html.DivElement() + ..style.setProperty('object-fit', 'cover') + ..append(videoElement); + + ui.platformViewRegistry.registerViewFactory( + _getViewType(textureId), + (_) => divElement, + ); + + final stream = await _getMediaStream(); + videoElement + ..autoplay = false + ..muted = !options.audio.enabled + ..srcObject = stream + ..setAttribute('playsinline', ''); + } + + Future _getMediaStream() async { + try { + final constraints = await options.toJson(); + return await window.navigator.mediaDevices!.getUserMedia(constraints); + } on html.DomException catch (e) { + switch (e.name) { + case 'NotFoundError': + case 'DevicesNotFoundError': + throw CameraException( + CameraErrorCodes.notFound, + 'No camera found for the given camera options.', + ); + case 'NotReadableError': + case 'TrackStartError': + throw CameraException( + CameraErrorCodes.notReadable, + 'The camera is not readable due to a hardware error ' + 'that prevented access to the device.', + ); + case 'OverconstrainedError': + case 'ConstraintNotSatisfiedError': + throw CameraException( + CameraErrorCodes.overconstrained, + 'The camera options are impossible to satisfy.', + ); + case 'NotAllowedError': + case 'PermissionDeniedError': + throw CameraException( + CameraErrorCodes.permissionDenied, + 'The camera cannot be used or the permission ' + 'to access the camera is not granted.', + ); + case 'TypeError': + throw CameraException( + CameraErrorCodes.type, + 'The camera options are incorrect or attempted' + 'to access the media input from an insecure context.', + ); + default: + throw CameraException( + CameraErrorCodes.unknown, + 'An unknown error occured when initializing the camera.', + ); + } + } catch (_) { + throw CameraException( + CameraErrorCodes.unknown, + 'An unknown error occured when initializing the camera.', + ); + } + } + + /// Starts the camera stream. + /// + /// Initializes the camera source if the camera was previously stopped. + Future play() async { + if (videoElement.srcObject == null) { + final stream = await _getMediaStream(); + videoElement.srcObject = stream; + } + await videoElement.play(); + } + + /// Stops the camera stream and resets the camera source. + void stop() { + final tracks = videoElement.srcObject?.getVideoTracks(); + if (tracks != null) { + for (final track in tracks) { + track.stop(); + } + } + videoElement.srcObject = null; + } + + /// Captures a picture and returns the saved file in a JPEG format. + Future takePicture() async { + final videoWidth = videoElement.videoWidth; + final videoHeight = videoElement.videoHeight; + final canvas = html.CanvasElement(width: videoWidth, height: videoHeight); + canvas.context2D + ..translate(videoWidth, 0) + ..scale(-1, 1) + ..drawImageScaled(videoElement, 0, 0, videoWidth, videoHeight); + final blob = await canvas.toBlob('image/jpeg'); + return XFile(html.Url.createObjectUrl(blob)); + } + + /// Disposes the camera by stopping the camera stream + /// and reloading the camera source. + void dispose() { + /// Stop the camera stream. + stop(); + + /// Reset the [videoElement] to its initial state. + videoElement + ..srcObject = null + ..load(); + } + + /// Applies default styles to the video [element]. + void _applyDefaultVideoStyles(html.VideoElement element) { + element.style + ..transformOrigin = 'center' + ..pointerEvents = 'none' + ..width = '100%' + ..height = '100%' + ..objectFit = 'cover' + ..transform = 'scaleX(-1)'; + } +} diff --git a/packages/camera/camera_web/lib/src/shims/dart_ui.dart b/packages/camera/camera_web/lib/src/shims/dart_ui.dart new file mode 100644 index 000000000000..5eacec5fe867 --- /dev/null +++ b/packages/camera/camera_web/lib/src/shims/dart_ui.dart @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// This file shims dart:ui in web-only scenarios, getting rid of the need to +/// suppress analyzer warnings. + +// TODO(flutter/flutter#55000) Remove this file once web-only dart:ui APIs +// are exposed from a dedicated place. +export 'dart_ui_fake.dart' if (dart.library.html) 'dart_ui_real.dart'; diff --git a/packages/camera/camera_web/lib/src/shims/dart_ui_fake.dart b/packages/camera/camera_web/lib/src/shims/dart_ui_fake.dart new file mode 100644 index 000000000000..f2862af8b704 --- /dev/null +++ b/packages/camera/camera_web/lib/src/shims/dart_ui_fake.dart @@ -0,0 +1,28 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:html' as html; + +// Fake interface for the logic that this package needs from (web-only) dart:ui. +// This is conditionally exported so the analyzer sees these methods as available. + +/// Shim for web_ui engine.PlatformViewRegistry +/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62 +class platformViewRegistry { + /// Shim for registerViewFactory + /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72 + static registerViewFactory( + String viewTypeId, html.Element Function(int viewId) viewFactory) {} +} + +/// Shim for web_ui engine.AssetManager. +/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L12 +class webOnlyAssetManager { + /// Shim for getAssetUrl. + /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L45 + static getAssetUrl(String asset) {} +} + +/// Signature of callbacks that have no arguments and return no data. +typedef VoidCallback = void Function(); diff --git a/packages/camera/camera_web/lib/src/shims/dart_ui_real.dart b/packages/camera/camera_web/lib/src/shims/dart_ui_real.dart new file mode 100644 index 000000000000..276b768c76c5 --- /dev/null +++ b/packages/camera/camera_web/lib/src/shims/dart_ui_real.dart @@ -0,0 +1,5 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'dart:ui'; diff --git a/packages/camera/camera_web/lib/src/types/camera_error_codes.dart b/packages/camera/camera_web/lib/src/types/camera_error_codes.dart new file mode 100644 index 000000000000..7afc08f7ca76 --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/camera_error_codes.dart @@ -0,0 +1,25 @@ +/// Error codes that may occur during the camera initialization or streaming. +abstract class CameraErrorCodes { + /// The camera is not supported. + static const notSupported = 'cameraNotSupported'; + + /// The camera is not found. + static const notFound = 'cameraNotFound'; + + /// The camera is not readable. + static const notReadable = 'cameraNotReadable'; + + /// The camera options are impossible to satisfy. + static const overconstrained = 'cameraOverconstrained'; + + /// The camera cannot be used or the permission + /// to access the camera is not granted. + static const permissionDenied = 'cameraPermission'; + + /// The camera options are incorrect or attempted + /// to access the media input from an insecure context. + static const type = 'cameraType'; + + /// An unknown camera error. + static const unknown = 'cameraUnknown'; +} From b7873438770c9b4b4d8b78c2d563beeef7f436ed Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Tue, 13 Jul 2021 16:50:24 +0200 Subject: [PATCH 2/7] test: add camera tests --- .../example/integration_test/camera_test.dart | 484 ++++++++++++++++++ 1 file changed, 484 insertions(+) create mode 100644 packages/camera/camera_web/example/integration_test/camera_test.dart diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart new file mode 100644 index 000000000000..9f031c127d41 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -0,0 +1,484 @@ +// ignore_for_file: prefer_const_constructors + +import 'dart:html'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/camera.dart'; +import 'package:camera_web/src/types/camera_error_codes.dart'; +import 'package:camera_web/src/types/camera_options.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Camera', () { + late Window window; + late Navigator navigator; + late MediaStream mediaStream; + late MediaDevices mediaDevices; + + setUp(() { + window = MockWindow(); + navigator = MockNavigator(); + mediaDevices = MockMediaDevices(); + + final videoElement = VideoElement() + ..src = 'https://www.w3schools.com/tags/mov_bbb.mp4' + ..preload = 'true' + ..width = 10 + ..height = 10; + + mediaStream = videoElement.captureStream(); + + when(() => window.navigator).thenReturn(navigator); + when(() => navigator.mediaDevices).thenReturn(mediaDevices); + when( + () => mediaDevices.getUserMedia(any()), + ).thenAnswer((_) async => mediaStream); + }); + + group('initialize', () { + testWidgets( + 'creates a video element ' + 'with correct properties', (tester) async { + const audioConstraints = AudioConstraints(enabled: true); + + final camera = Camera( + textureId: 1, + options: CameraOptions( + audio: audioConstraints, + ), + window: window, + ); + + await camera.initialize(); + + expect(camera.videoElement, isNotNull); + expect(camera.videoElement.autoplay, isFalse); + expect(camera.videoElement.muted, !audioConstraints.enabled); + expect(camera.videoElement.srcObject, mediaStream); + expect(camera.videoElement.attributes.keys, contains('playsinline')); + + expect( + camera.videoElement.style.transformOrigin, equals('center center')); + expect(camera.videoElement.style.pointerEvents, equals('none')); + expect(camera.videoElement.style.width, equals('100%')); + expect(camera.videoElement.style.height, equals('100%')); + expect(camera.videoElement.style.objectFit, equals('cover')); + expect(camera.videoElement.style.transform, equals('scaleX(-1)')); + }); + + testWidgets( + 'creates a wrapping div element ' + 'with correct properties', (tester) async { + final camera = Camera( + textureId: 1, + window: window, + ); + + await camera.initialize(); + + expect(camera.divElement, isNotNull); + expect(camera.divElement.style.objectFit, equals('cover')); + expect(camera.divElement.children, contains(camera.videoElement)); + }); + + testWidgets('calls getUserMedia with provided options', (tester) async { + final options = CameraOptions( + video: VideoConstraints( + facingMode: FacingModeConstraint.exact(CameraType.user), + width: VideoSizeConstraint(ideal: 200), + ), + ); + + final optionsJson = await options.toJson(); + + final camera = Camera( + textureId: 1, + options: options, + window: window, + ); + + await camera.initialize(); + + verify(() => mediaDevices.getUserMedia(optionsJson)).called(1); + }); + + group('throws CameraException', () { + testWidgets( + 'containing notSupported error ' + 'when there are no media devices', (tester) async { + when(() => navigator.mediaDevices).thenReturn(null); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notSupported, + ), + ), + ); + }); + + testWidgets( + 'containing notFound error ' + 'when getUserMedia throws DomException ' + 'with NotFoundError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('NotFoundError')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notFound, + ), + ), + ); + }); + + testWidgets( + 'containing notFound error ' + 'when getUserMedia throws DomException ' + 'with DevicesNotFoundError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('DevicesNotFoundError')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notFound, + ), + ), + ); + }); + + testWidgets( + 'containing notReadable error ' + 'when getUserMedia throws DomException ' + 'with NotReadableError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('NotReadableError')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notReadable, + ), + ), + ); + }); + + testWidgets( + 'containing notReadable error ' + 'when getUserMedia throws DomException ' + 'with TrackStartError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('TrackStartError')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notReadable, + ), + ), + ); + }); + + testWidgets( + 'containing overconstrained error ' + 'when getUserMedia throws DomException ' + 'with OverconstrainedError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('OverconstrainedError')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.overconstrained, + ), + ), + ); + }); + + testWidgets( + 'containing overconstrained error ' + 'when getUserMedia throws DomException ' + 'with ConstraintNotSatisfiedError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('ConstraintNotSatisfiedError')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.overconstrained, + ), + ), + ); + }); + + testWidgets( + 'containing permissionDenied error ' + 'when getUserMedia throws DomException ' + 'with NotAllowedError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('NotAllowedError')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.permissionDenied, + ), + ), + ); + }); + + testWidgets( + 'containing permissionDenied error ' + 'when getUserMedia throws DomException ' + 'with PermissionDeniedError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('PermissionDeniedError')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.permissionDenied, + ), + ), + ); + }); + + testWidgets( + 'containing type error ' + 'when getUserMedia throws DomException ' + 'with TypeError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('TypeError')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.type, + ), + ), + ); + }); + + testWidgets( + 'containing unknown error ' + 'when getUserMedia throws DomException ' + 'with an unknown error', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('Unknown')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.unknown, + ), + ), + ); + }); + + testWidgets( + 'containing unknown error ' + 'when getUserMedia throws an unknown exception', (tester) async { + when(() => mediaDevices.getUserMedia(any())).thenThrow(Exception()); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.unknown, + ), + ), + ); + }); + }); + }); + + group('play', () { + testWidgets('starts playing the video element', (tester) async { + var startedPlaying = false; + + final camera = Camera( + textureId: 1, + window: window, + ); + + await camera.initialize(); + + camera.videoElement.onPlay.listen((event) => startedPlaying = true); + + await camera.play(); + + expect(startedPlaying, isTrue); + }); + + testWidgets( + 'assigns media stream to the video element\'s source ' + 'if it does not exist', (tester) async { + final camera = Camera( + textureId: 1, + window: window, + ); + + await camera.initialize(); + + /// Remove the video element's source + /// by stopping the camera. + // ignore: cascade_invocations + camera.stop(); + + await camera.play(); + + expect(camera.videoElement.srcObject, mediaStream); + }); + }); + + group('stop', () { + testWidgets('resets the video element\'s source', (tester) async { + final camera = Camera( + textureId: 1, + window: window, + ); + + await camera.initialize(); + await camera.play(); + + camera.stop(); + + expect(camera.videoElement.srcObject, isNull); + }); + }); + + group('takePicture', () { + testWidgets('returns a captured picture', (tester) async { + final camera = Camera( + textureId: 1, + window: window, + ); + + await camera.initialize(); + await camera.play(); + + final pictureFile = await camera.takePicture(); + + expect(pictureFile, isNotNull); + }); + }); + + group('dispose', () { + testWidgets('resets the video element\'s source', (tester) async { + final camera = Camera( + textureId: 1, + window: window, + ); + + await camera.initialize(); + + camera.dispose(); + + expect(camera.videoElement.srcObject, isNull); + }); + }); + }); +} From 607feec2ce4a394ffa883013ea9214d9a7eb4bd1 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Wed, 14 Jul 2021 09:46:37 +0200 Subject: [PATCH 3/7] docs: add copyright --- .../camera_web/example/integration_test/camera_test.dart | 4 +++- packages/camera/camera_web/lib/src/camera.dart | 4 ++++ .../camera/camera_web/lib/src/types/camera_error_codes.dart | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) 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 9f031c127d41..1f2a4e65028f 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -1,4 +1,6 @@ -// ignore_for_file: prefer_const_constructors +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. import 'dart:html'; diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index e4d8023d5db3..164ccb180e5e 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -1,3 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'dart:html' as html; import 'shims/dart_ui.dart' as ui; diff --git a/packages/camera/camera_web/lib/src/types/camera_error_codes.dart b/packages/camera/camera_web/lib/src/types/camera_error_codes.dart index 7afc08f7ca76..f8dc5dfc4e32 100644 --- a/packages/camera/camera_web/lib/src/types/camera_error_codes.dart +++ b/packages/camera/camera_web/lib/src/types/camera_error_codes.dart @@ -1,3 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + /// Error codes that may occur during the camera initialization or streaming. abstract class CameraErrorCodes { /// The camera is not supported. From 725f6c4a8b4bee7eae0a7fecb9f2459141de191e Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Wed, 14 Jul 2021 09:50:44 +0200 Subject: [PATCH 4/7] test: update test video source --- .../camera_web/example/integration_test/camera_test.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 1f2a4e65028f..8af0aa593a62 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -29,7 +29,8 @@ void main() { mediaDevices = MockMediaDevices(); final videoElement = VideoElement() - ..src = 'https://www.w3schools.com/tags/mov_bbb.mp4' + ..src = + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4' ..preload = 'true' ..width = 10 ..height = 10; From ea7fe125c2ec4ed5cbaaee9d8be9efb44da9d87c Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 15 Jul 2021 09:36:00 +0200 Subject: [PATCH 5/7] feat: stopping the camera stops all tracks --- packages/camera/camera_web/lib/src/camera.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index 164ccb180e5e..41692d548882 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -149,7 +149,7 @@ class Camera { /// Stops the camera stream and resets the camera source. void stop() { - final tracks = videoElement.srcObject?.getVideoTracks(); + final tracks = videoElement.srcObject?.getTracks(); if (tracks != null) { for (final track in tracks) { track.stop(); From 9607c0c6de4312ddf8773f15367755adf6a1665a Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 15 Jul 2021 09:36:27 +0200 Subject: [PATCH 6/7] test: update camera test names --- .../example/integration_test/camera_test.dart | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) 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 8af0aa593a62..0f1dcf7049d9 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -113,7 +113,7 @@ void main() { group('throws CameraException', () { testWidgets( - 'containing notSupported error ' + 'with notSupported error ' 'when there are no media devices', (tester) async { when(() => navigator.mediaDevices).thenReturn(null); @@ -135,7 +135,7 @@ void main() { }); testWidgets( - 'containing notFound error ' + 'with notFound error ' 'when getUserMedia throws DomException ' 'with NotFoundError', (tester) async { when(() => mediaDevices.getUserMedia(any())) @@ -159,7 +159,7 @@ void main() { }); testWidgets( - 'containing notFound error ' + 'with notFound error ' 'when getUserMedia throws DomException ' 'with DevicesNotFoundError', (tester) async { when(() => mediaDevices.getUserMedia(any())) @@ -183,7 +183,7 @@ void main() { }); testWidgets( - 'containing notReadable error ' + 'with notReadable error ' 'when getUserMedia throws DomException ' 'with NotReadableError', (tester) async { when(() => mediaDevices.getUserMedia(any())) @@ -207,7 +207,7 @@ void main() { }); testWidgets( - 'containing notReadable error ' + 'with notReadable error ' 'when getUserMedia throws DomException ' 'with TrackStartError', (tester) async { when(() => mediaDevices.getUserMedia(any())) @@ -231,7 +231,7 @@ void main() { }); testWidgets( - 'containing overconstrained error ' + 'with overconstrained error ' 'when getUserMedia throws DomException ' 'with OverconstrainedError', (tester) async { when(() => mediaDevices.getUserMedia(any())) @@ -255,7 +255,7 @@ void main() { }); testWidgets( - 'containing overconstrained error ' + 'with overconstrained error ' 'when getUserMedia throws DomException ' 'with ConstraintNotSatisfiedError', (tester) async { when(() => mediaDevices.getUserMedia(any())) @@ -279,7 +279,7 @@ void main() { }); testWidgets( - 'containing permissionDenied error ' + 'with permissionDenied error ' 'when getUserMedia throws DomException ' 'with NotAllowedError', (tester) async { when(() => mediaDevices.getUserMedia(any())) @@ -303,7 +303,7 @@ void main() { }); testWidgets( - 'containing permissionDenied error ' + 'with permissionDenied error ' 'when getUserMedia throws DomException ' 'with PermissionDeniedError', (tester) async { when(() => mediaDevices.getUserMedia(any())) @@ -327,7 +327,7 @@ void main() { }); testWidgets( - 'containing type error ' + 'with type error ' 'when getUserMedia throws DomException ' 'with TypeError', (tester) async { when(() => mediaDevices.getUserMedia(any())) @@ -351,7 +351,7 @@ void main() { }); testWidgets( - 'containing unknown error ' + 'with unknown error ' 'when getUserMedia throws DomException ' 'with an unknown error', (tester) async { when(() => mediaDevices.getUserMedia(any())) @@ -375,7 +375,7 @@ void main() { }); testWidgets( - 'containing unknown error ' + 'with unknown error ' 'when getUserMedia throws an unknown exception', (tester) async { when(() => mediaDevices.getUserMedia(any())).thenThrow(Exception()); From b603302892fe8d1a113b5d39192b3519658806aa Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 16 Jul 2021 12:31:15 +0200 Subject: [PATCH 7/7] feat: add camera error codes to types barrel file --- packages/camera/camera_web/lib/src/types/types.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/camera/camera_web/lib/src/types/types.dart b/packages/camera/camera_web/lib/src/types/types.dart index deccd32da4c0..fc1f931679ff 100644 --- a/packages/camera/camera_web/lib/src/types/types.dart +++ b/packages/camera/camera_web/lib/src/types/types.dart @@ -2,4 +2,5 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'camera_error_codes.dart'; export 'camera_options.dart';