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 f331cc1485ab..d498b2596dbe 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -85,11 +85,17 @@ void main() { 'creates a video element ' 'with correct properties', (tester) async { const audioConstraints = AudioConstraints(enabled: true); + final videoConstraints = VideoConstraints( + facingMode: FacingModeConstraint( + CameraType.user, + ), + ); final camera = Camera( textureId: textureId, options: CameraOptions( audio: audioConstraints, + video: videoConstraints, ), cameraService: cameraService, ); @@ -108,6 +114,27 @@ void main() { expect(camera.videoElement.style.width, equals('100%')); expect(camera.videoElement.style.height, equals('100%')); expect(camera.videoElement.style.objectFit, equals('cover')); + }); + + testWidgets( + 'flips the video element horizontally ' + 'for a back camera', (tester) async { + final videoConstraints = VideoConstraints( + facingMode: FacingModeConstraint( + CameraType.environment, + ), + ); + + final camera = Camera( + textureId: textureId, + options: CameraOptions( + video: videoConstraints, + ), + cameraService: cameraService, + ); + + await camera.initialize(); + expect(camera.videoElement.style.transform, equals('scaleX(-1)')); }); @@ -376,7 +403,7 @@ void main() { await camera.initialize(); expect( - await camera.getVideoSize(), + camera.getVideoSize(), equals(videoSize), ); }); @@ -396,7 +423,7 @@ void main() { await camera.initialize(); expect( - await camera.getVideoSize(), + camera.getVideoSize(), equals(Size.zero), ); }); @@ -819,6 +846,87 @@ void main() { }); }); + group('getLensDirection', () { + testWidgets( + 'returns a lens direction ' + 'based on the first video track settings', (tester) async { + final videoElement = MockVideoElement(); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + )..videoElement = videoElement; + + final firstVideoTrack = MockMediaStreamTrack(); + + when(() => videoElement.srcObject).thenReturn( + FakeMediaStream([ + firstVideoTrack, + MockMediaStreamTrack(), + ]), + ); + + when(firstVideoTrack.getSettings) + .thenReturn({'facingMode': 'environment'}); + + when(() => cameraService.mapFacingModeToLensDirection('environment')) + .thenReturn(CameraLensDirection.external); + + expect( + camera.getLensDirection(), + equals(CameraLensDirection.external), + ); + }); + + testWidgets( + 'returns null ' + 'if the first video track is missing the facing mode', + (tester) async { + final videoElement = MockVideoElement(); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + )..videoElement = videoElement; + + final firstVideoTrack = MockMediaStreamTrack(); + + when(() => videoElement.srcObject).thenReturn( + FakeMediaStream([ + firstVideoTrack, + MockMediaStreamTrack(), + ]), + ); + + when(firstVideoTrack.getSettings).thenReturn({}); + + expect( + camera.getLensDirection(), + isNull, + ); + }); + + testWidgets( + 'returns null ' + 'if the camera is missing video tracks', (tester) async { + // Create a video stream with no video tracks. + final videoElement = VideoElement(); + mediaStream = videoElement.captureStream(); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + + expect( + camera.getLensDirection(), + isNull, + ); + }); + }); + group('getViewType', () { testWidgets('returns a correct view type', (tester) async { final camera = Camera( diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 9ab8c511f753..7db977729241 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -574,9 +574,7 @@ void main() { abortStreamController = StreamController(); endedStreamController = StreamController(); - when(camera.getVideoSize).thenAnswer( - (_) => Future.value(Size(10, 10)), - ); + when(camera.getVideoSize).thenReturn(Size(10, 10)); when(camera.initialize).thenAnswer((_) => Future.value()); when(camera.play).thenAnswer((_) => Future.value()); @@ -1660,9 +1658,7 @@ void main() { abortStreamController = StreamController(); endedStreamController = StreamController(); - when(camera.getVideoSize).thenAnswer( - (_) => Future.value(Size(10, 10)), - ); + when(camera.getVideoSize).thenReturn(Size(10, 10)); when(camera.initialize).thenAnswer((_) => Future.value()); when(camera.play).thenAnswer((_) => Future.value()); when(camera.dispose).thenAnswer((_) => Future.value()); @@ -1818,9 +1814,7 @@ void main() { abortStreamController = StreamController(); endedStreamController = StreamController(); - when(camera.getVideoSize).thenAnswer( - (_) => Future.value(Size(10, 10)), - ); + when(camera.getVideoSize).thenReturn(Size(10, 10)); when(camera.initialize).thenAnswer((_) => Future.value()); when(camera.play).thenAnswer((_) => Future.value()); diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index 74d8546fbb12..cc5f0dee69fe 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -106,7 +106,6 @@ class Camera { ); videoElement = html.VideoElement(); - _applyDefaultVideoStyles(videoElement); divElement = html.DivElement() ..style.setProperty('object-fit', 'cover') @@ -123,6 +122,8 @@ class Camera { ..srcObject = stream ..setAttribute('playsinline', ''); + _applyDefaultVideoStyles(videoElement); + final videoTracks = stream!.getVideoTracks(); if (videoTracks.isNotEmpty) { @@ -149,7 +150,7 @@ class Camera { } /// Pauses the camera stream on the current frame. - void pause() async { + void pause() { videoElement.pause(); } @@ -185,11 +186,17 @@ class Camera { final videoWidth = videoElement.videoWidth; final videoHeight = videoElement.videoHeight; final canvas = html.CanvasElement(width: videoWidth, height: videoHeight); + final isBackCamera = getLensDirection() == CameraLensDirection.back; + + // Flip the picture horizontally if it is not taken from a back camera. + if (!isBackCamera) { + canvas.context2D + ..translate(videoWidth, 0) + ..scale(-1, 1); + } canvas.context2D - ..translate(videoWidth, 0) - ..scale(-1, 1) - ..drawImageScaled(videoElement, 0, 0, videoWidth, videoHeight); + .drawImageScaled(videoElement, 0, 0, videoWidth, videoHeight); final blob = await canvas.toBlob('image/jpeg'); @@ -204,7 +211,7 @@ class Camera { /// /// Returns [Size.zero] if the camera is missing a video track or /// the video track does not include the width or height setting. - Future getVideoSize() async { + Size getVideoSize() { final videoTracks = videoElement.srcObject?.getVideoTracks() ?? []; if (videoTracks.isEmpty) { @@ -332,6 +339,29 @@ class Camera { }); } + /// Returns a lens direction of this camera. + /// + /// Returns null if the camera is missing a video track or + /// the video track does not include the facing mode setting. + CameraLensDirection? getLensDirection() { + final videoTracks = videoElement.srcObject?.getVideoTracks() ?? []; + + if (videoTracks.isEmpty) { + return null; + } + + final defaultVideoTrack = videoTracks.first; + final defaultVideoTrackSettings = defaultVideoTrack.getSettings(); + + final facingMode = defaultVideoTrackSettings['facingMode']; + + if (facingMode != null) { + return _cameraService.mapFacingModeToLensDirection(facingMode); + } else { + return null; + } + } + /// Returns the registered view type of the camera. String getViewType() => _getViewType(textureId); @@ -354,12 +384,18 @@ class Camera { /// Applies default styles to the video [element]. void _applyDefaultVideoStyles(html.VideoElement element) { + final isBackCamera = getLensDirection() == CameraLensDirection.back; + + // Flip the video horizontally if it is not taken from a back camera. + if (!isBackCamera) { + element.style.transform = 'scaleX(-1)'; + } + element.style ..transformOrigin = 'center' ..pointerEvents = 'none' ..width = '100%' ..height = '100%' - ..objectFit = 'cover' - ..transform = 'scaleX(-1)'; + ..objectFit = 'cover'; } } diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 19ee43f36660..76c522240b36 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -285,7 +285,7 @@ class CameraPlugin extends CameraPlatform { ); }); - final cameraSize = await camera.getVideoSize(); + final cameraSize = camera.getVideoSize(); cameraEventStreamController.add( CameraInitializedEvent(