Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 18129d6

Browse files
authored
[camera_web] Do not flip the video on the back camera (#4281)
* feat: do not flip the video on the back camera * test: do not flip the video on the back camera tests * feat: update getVideoSize usage
1 parent 32ca776 commit 18129d6

File tree

4 files changed

+158
-20
lines changed

4 files changed

+158
-20
lines changed

packages/camera/camera_web/example/integration_test/camera_test.dart

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,17 @@ void main() {
8585
'creates a video element '
8686
'with correct properties', (tester) async {
8787
const audioConstraints = AudioConstraints(enabled: true);
88+
final videoConstraints = VideoConstraints(
89+
facingMode: FacingModeConstraint(
90+
CameraType.user,
91+
),
92+
);
8893

8994
final camera = Camera(
9095
textureId: textureId,
9196
options: CameraOptions(
9297
audio: audioConstraints,
98+
video: videoConstraints,
9399
),
94100
cameraService: cameraService,
95101
);
@@ -108,6 +114,27 @@ void main() {
108114
expect(camera.videoElement.style.width, equals('100%'));
109115
expect(camera.videoElement.style.height, equals('100%'));
110116
expect(camera.videoElement.style.objectFit, equals('cover'));
117+
});
118+
119+
testWidgets(
120+
'flips the video element horizontally '
121+
'for a back camera', (tester) async {
122+
final videoConstraints = VideoConstraints(
123+
facingMode: FacingModeConstraint(
124+
CameraType.environment,
125+
),
126+
);
127+
128+
final camera = Camera(
129+
textureId: textureId,
130+
options: CameraOptions(
131+
video: videoConstraints,
132+
),
133+
cameraService: cameraService,
134+
);
135+
136+
await camera.initialize();
137+
111138
expect(camera.videoElement.style.transform, equals('scaleX(-1)'));
112139
});
113140

@@ -376,7 +403,7 @@ void main() {
376403
await camera.initialize();
377404

378405
expect(
379-
await camera.getVideoSize(),
406+
camera.getVideoSize(),
380407
equals(videoSize),
381408
);
382409
});
@@ -396,7 +423,7 @@ void main() {
396423
await camera.initialize();
397424

398425
expect(
399-
await camera.getVideoSize(),
426+
camera.getVideoSize(),
400427
equals(Size.zero),
401428
);
402429
});
@@ -819,6 +846,87 @@ void main() {
819846
});
820847
});
821848

849+
group('getLensDirection', () {
850+
testWidgets(
851+
'returns a lens direction '
852+
'based on the first video track settings', (tester) async {
853+
final videoElement = MockVideoElement();
854+
855+
final camera = Camera(
856+
textureId: textureId,
857+
cameraService: cameraService,
858+
)..videoElement = videoElement;
859+
860+
final firstVideoTrack = MockMediaStreamTrack();
861+
862+
when(() => videoElement.srcObject).thenReturn(
863+
FakeMediaStream([
864+
firstVideoTrack,
865+
MockMediaStreamTrack(),
866+
]),
867+
);
868+
869+
when(firstVideoTrack.getSettings)
870+
.thenReturn({'facingMode': 'environment'});
871+
872+
when(() => cameraService.mapFacingModeToLensDirection('environment'))
873+
.thenReturn(CameraLensDirection.external);
874+
875+
expect(
876+
camera.getLensDirection(),
877+
equals(CameraLensDirection.external),
878+
);
879+
});
880+
881+
testWidgets(
882+
'returns null '
883+
'if the first video track is missing the facing mode',
884+
(tester) async {
885+
final videoElement = MockVideoElement();
886+
887+
final camera = Camera(
888+
textureId: textureId,
889+
cameraService: cameraService,
890+
)..videoElement = videoElement;
891+
892+
final firstVideoTrack = MockMediaStreamTrack();
893+
894+
when(() => videoElement.srcObject).thenReturn(
895+
FakeMediaStream([
896+
firstVideoTrack,
897+
MockMediaStreamTrack(),
898+
]),
899+
);
900+
901+
when(firstVideoTrack.getSettings).thenReturn({});
902+
903+
expect(
904+
camera.getLensDirection(),
905+
isNull,
906+
);
907+
});
908+
909+
testWidgets(
910+
'returns null '
911+
'if the camera is missing video tracks', (tester) async {
912+
// Create a video stream with no video tracks.
913+
final videoElement = VideoElement();
914+
mediaStream = videoElement.captureStream();
915+
916+
final camera = Camera(
917+
textureId: textureId,
918+
cameraService: cameraService,
919+
);
920+
921+
await camera.initialize();
922+
923+
expect(
924+
camera.getLensDirection(),
925+
isNull,
926+
);
927+
});
928+
});
929+
822930
group('getViewType', () {
823931
testWidgets('returns a correct view type', (tester) async {
824932
final camera = Camera(

packages/camera/camera_web/example/integration_test/camera_web_test.dart

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -574,9 +574,7 @@ void main() {
574574
abortStreamController = StreamController<Event>();
575575
endedStreamController = StreamController<MediaStreamTrack>();
576576

577-
when(camera.getVideoSize).thenAnswer(
578-
(_) => Future.value(Size(10, 10)),
579-
);
577+
when(camera.getVideoSize).thenReturn(Size(10, 10));
580578
when(camera.initialize).thenAnswer((_) => Future.value());
581579
when(camera.play).thenAnswer((_) => Future.value());
582580

@@ -1660,9 +1658,7 @@ void main() {
16601658
abortStreamController = StreamController<Event>();
16611659
endedStreamController = StreamController<MediaStreamTrack>();
16621660

1663-
when(camera.getVideoSize).thenAnswer(
1664-
(_) => Future.value(Size(10, 10)),
1665-
);
1661+
when(camera.getVideoSize).thenReturn(Size(10, 10));
16661662
when(camera.initialize).thenAnswer((_) => Future.value());
16671663
when(camera.play).thenAnswer((_) => Future.value());
16681664
when(camera.dispose).thenAnswer((_) => Future.value());
@@ -1818,9 +1814,7 @@ void main() {
18181814
abortStreamController = StreamController<Event>();
18191815
endedStreamController = StreamController<MediaStreamTrack>();
18201816

1821-
when(camera.getVideoSize).thenAnswer(
1822-
(_) => Future.value(Size(10, 10)),
1823-
);
1817+
when(camera.getVideoSize).thenReturn(Size(10, 10));
18241818
when(camera.initialize).thenAnswer((_) => Future.value());
18251819
when(camera.play).thenAnswer((_) => Future.value());
18261820

packages/camera/camera_web/lib/src/camera.dart

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ class Camera {
106106
);
107107

108108
videoElement = html.VideoElement();
109-
_applyDefaultVideoStyles(videoElement);
110109

111110
divElement = html.DivElement()
112111
..style.setProperty('object-fit', 'cover')
@@ -123,6 +122,8 @@ class Camera {
123122
..srcObject = stream
124123
..setAttribute('playsinline', '');
125124

125+
_applyDefaultVideoStyles(videoElement);
126+
126127
final videoTracks = stream!.getVideoTracks();
127128

128129
if (videoTracks.isNotEmpty) {
@@ -149,7 +150,7 @@ class Camera {
149150
}
150151

151152
/// Pauses the camera stream on the current frame.
152-
void pause() async {
153+
void pause() {
153154
videoElement.pause();
154155
}
155156

@@ -185,11 +186,17 @@ class Camera {
185186
final videoWidth = videoElement.videoWidth;
186187
final videoHeight = videoElement.videoHeight;
187188
final canvas = html.CanvasElement(width: videoWidth, height: videoHeight);
189+
final isBackCamera = getLensDirection() == CameraLensDirection.back;
190+
191+
// Flip the picture horizontally if it is not taken from a back camera.
192+
if (!isBackCamera) {
193+
canvas.context2D
194+
..translate(videoWidth, 0)
195+
..scale(-1, 1);
196+
}
188197

189198
canvas.context2D
190-
..translate(videoWidth, 0)
191-
..scale(-1, 1)
192-
..drawImageScaled(videoElement, 0, 0, videoWidth, videoHeight);
199+
.drawImageScaled(videoElement, 0, 0, videoWidth, videoHeight);
193200

194201
final blob = await canvas.toBlob('image/jpeg');
195202

@@ -204,7 +211,7 @@ class Camera {
204211
///
205212
/// Returns [Size.zero] if the camera is missing a video track or
206213
/// the video track does not include the width or height setting.
207-
Future<Size> getVideoSize() async {
214+
Size getVideoSize() {
208215
final videoTracks = videoElement.srcObject?.getVideoTracks() ?? [];
209216

210217
if (videoTracks.isEmpty) {
@@ -332,6 +339,29 @@ class Camera {
332339
});
333340
}
334341

342+
/// Returns a lens direction of this camera.
343+
///
344+
/// Returns null if the camera is missing a video track or
345+
/// the video track does not include the facing mode setting.
346+
CameraLensDirection? getLensDirection() {
347+
final videoTracks = videoElement.srcObject?.getVideoTracks() ?? [];
348+
349+
if (videoTracks.isEmpty) {
350+
return null;
351+
}
352+
353+
final defaultVideoTrack = videoTracks.first;
354+
final defaultVideoTrackSettings = defaultVideoTrack.getSettings();
355+
356+
final facingMode = defaultVideoTrackSettings['facingMode'];
357+
358+
if (facingMode != null) {
359+
return _cameraService.mapFacingModeToLensDirection(facingMode);
360+
} else {
361+
return null;
362+
}
363+
}
364+
335365
/// Returns the registered view type of the camera.
336366
String getViewType() => _getViewType(textureId);
337367

@@ -354,12 +384,18 @@ class Camera {
354384

355385
/// Applies default styles to the video [element].
356386
void _applyDefaultVideoStyles(html.VideoElement element) {
387+
final isBackCamera = getLensDirection() == CameraLensDirection.back;
388+
389+
// Flip the video horizontally if it is not taken from a back camera.
390+
if (!isBackCamera) {
391+
element.style.transform = 'scaleX(-1)';
392+
}
393+
357394
element.style
358395
..transformOrigin = 'center'
359396
..pointerEvents = 'none'
360397
..width = '100%'
361398
..height = '100%'
362-
..objectFit = 'cover'
363-
..transform = 'scaleX(-1)';
399+
..objectFit = 'cover';
364400
}
365401
}

packages/camera/camera_web/lib/src/camera_web.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ class CameraPlugin extends CameraPlatform {
285285
);
286286
});
287287

288-
final cameraSize = await camera.getVideoSize();
288+
final cameraSize = camera.getVideoSize();
289289

290290
cameraEventStreamController.add(
291291
CameraInitializedEvent(

0 commit comments

Comments
 (0)