Skip to content

Commit fb8808d

Browse files
bselweamantoux
authored andcommitted
[camera_web] Add initializeCamera implementation (flutter#4186)
1 parent 47c3429 commit fb8808d

File tree

5 files changed

+260
-39
lines changed

5 files changed

+260
-39
lines changed

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

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'dart:html';
6+
import 'dart:ui';
67

78
import 'package:camera_platform_interface/camera_platform_interface.dart';
89
import 'package:camera_web/src/camera.dart';
@@ -28,13 +29,7 @@ void main() {
2829
navigator = MockNavigator();
2930
mediaDevices = MockMediaDevices();
3031

31-
final videoElement = VideoElement()
32-
..src =
33-
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'
34-
..preload = 'true'
35-
..width = 10
36-
..height = 10;
37-
32+
final videoElement = getVideoElementWithBlankStream(Size(10, 10));
3833
mediaStream = videoElement.captureStream();
3934

4035
when(() => window.navigator).thenReturn(navigator);
@@ -469,6 +464,49 @@ void main() {
469464
});
470465
});
471466

467+
group('getVideoSize', () {
468+
testWidgets(
469+
'returns a size '
470+
'based on the first video track settings', (tester) async {
471+
const videoSize = Size(1280, 720);
472+
473+
final videoElement = getVideoElementWithBlankStream(videoSize);
474+
mediaStream = videoElement.captureStream();
475+
476+
final camera = Camera(
477+
textureId: 1,
478+
window: window,
479+
);
480+
481+
await camera.initialize();
482+
483+
expect(
484+
await camera.getVideoSize(),
485+
equals(videoSize),
486+
);
487+
});
488+
489+
testWidgets(
490+
'returns Size.zero '
491+
'if the camera is missing video tracks', (tester) async {
492+
// Create a video stream with no video tracks.
493+
final videoElement = VideoElement();
494+
mediaStream = videoElement.captureStream();
495+
496+
final camera = Camera(
497+
textureId: 1,
498+
window: window,
499+
);
500+
501+
await camera.initialize();
502+
503+
expect(
504+
await camera.getVideoSize(),
505+
equals(Size.zero),
506+
);
507+
});
508+
});
509+
472510
group('dispose', () {
473511
testWidgets('resets the video element\'s source', (tester) async {
474512
final camera = Camera(

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

Lines changed: 114 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'dart:html';
66
import 'dart:ui';
77

8+
import 'package:async/async.dart';
89
import 'package:camera_platform_interface/camera_platform_interface.dart';
910
import 'package:camera_web/camera_web.dart';
1011
import 'package:camera_web/src/camera.dart';
@@ -33,13 +34,8 @@ void main() {
3334
window = MockWindow();
3435
navigator = MockNavigator();
3536
mediaDevices = MockMediaDevices();
36-
videoElement = VideoElement()
37-
..src =
38-
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'
39-
..preload = 'true'
40-
..width = 10
41-
..height = 10
42-
..crossOrigin = 'anonymous';
37+
38+
videoElement = getVideoElementWithBlankStream(Size(10, 10));
4339

4440
cameraSettings = MockCameraSettings();
4541

@@ -327,21 +323,18 @@ void main() {
327323
const ultraHighResolutionSize = Size(3840, 2160);
328324
const maxResolutionSize = Size(3840, 2160);
329325

330-
late CameraDescription cameraDescription;
331-
late CameraMetadata cameraMetadata;
332-
333-
setUp(() {
334-
cameraDescription = CameraDescription(
335-
name: 'name',
336-
lensDirection: CameraLensDirection.front,
337-
sensorOrientation: 0,
338-
);
326+
final cameraDescription = CameraDescription(
327+
name: 'name',
328+
lensDirection: CameraLensDirection.front,
329+
sensorOrientation: 0,
330+
);
339331

340-
cameraMetadata = CameraMetadata(
341-
deviceId: 'deviceId',
342-
facingMode: 'user',
343-
);
332+
final cameraMetadata = CameraMetadata(
333+
deviceId: 'deviceId',
334+
facingMode: 'user',
335+
);
344336

337+
setUp(() {
345338
// Add metadata for the camera description.
346339
(CameraPlatform.instance as CameraPlugin)
347340
.camerasMetadata[cameraDescription] = cameraMetadata;
@@ -434,11 +427,38 @@ void main() {
434427
});
435428
});
436429

437-
testWidgets('initializeCamera throws UnimplementedError', (tester) async {
438-
expect(
439-
() => CameraPlatform.instance.initializeCamera(cameraId),
440-
throwsUnimplementedError,
441-
);
430+
group('initializeCamera', () {
431+
testWidgets(
432+
'throws CameraException '
433+
'with notFound error '
434+
'if the camera does not exist', (tester) async {
435+
expect(
436+
() => CameraPlatform.instance.initializeCamera(cameraId),
437+
throwsA(
438+
isA<CameraException>().having(
439+
(e) => e.code,
440+
'code',
441+
CameraErrorCodes.notFound,
442+
),
443+
),
444+
);
445+
});
446+
447+
testWidgets('initializes and plays the camera', (tester) async {
448+
final camera = MockCamera();
449+
450+
when(camera.getVideoSize).thenAnswer((_) => Future.value(Size(10, 10)));
451+
when(camera.initialize).thenAnswer((_) => Future.value());
452+
when(camera.play).thenAnswer((_) => Future.value());
453+
454+
// Save the camera in the camera plugin.
455+
(CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera;
456+
457+
await CameraPlatform.instance.initializeCamera(cameraId);
458+
459+
verify(camera.initialize).called(1);
460+
verify(camera.play).called(1);
461+
});
442462
});
443463

444464
testWidgets('lockCaptureOrientation throws UnimplementedError',
@@ -628,13 +648,78 @@ void main() {
628648
);
629649
});
630650

651+
group('getCamera', () {
652+
testWidgets('returns the correct camera', (tester) async {
653+
final camera = Camera(textureId: cameraId, window: window);
654+
655+
// Save the camera in the camera plugin.
656+
(CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera;
657+
658+
expect(
659+
(CameraPlatform.instance as CameraPlugin).getCamera(cameraId),
660+
equals(camera),
661+
);
662+
});
663+
664+
testWidgets(
665+
'throws CameraException '
666+
'with notFound error '
667+
'if the camera does not exist', (tester) async {
668+
expect(
669+
() => (CameraPlatform.instance as CameraPlugin).getCamera(cameraId),
670+
throwsA(
671+
isA<CameraException>().having(
672+
(e) => e.code,
673+
'code',
674+
CameraErrorCodes.notFound,
675+
),
676+
),
677+
);
678+
});
679+
});
680+
631681
group('events', () {
632-
testWidgets('onCameraInitialized throws UnimplementedError',
633-
(tester) async {
682+
testWidgets(
683+
'onCameraInitialized emits a CameraInitializedEvent '
684+
'on initializeCamera', (tester) async {
685+
// Mock the camera to use a blank video stream of size 1280x720.
686+
const videoSize = Size(1280, 720);
687+
688+
videoElement = getVideoElementWithBlankStream(videoSize);
689+
690+
when(
691+
() => mediaDevices.getUserMedia(any()),
692+
).thenAnswer((_) async => videoElement.captureStream());
693+
694+
final camera = Camera(
695+
textureId: cameraId,
696+
window: window,
697+
);
698+
699+
// Save the camera in the camera plugin.
700+
(CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera;
701+
702+
final Stream<CameraInitializedEvent> eventStream =
703+
CameraPlatform.instance.onCameraInitialized(cameraId);
704+
705+
final streamQueue = StreamQueue(eventStream);
706+
707+
await CameraPlatform.instance.initializeCamera(cameraId);
708+
634709
expect(
635-
() => CameraPlatform.instance.onCameraInitialized(cameraId),
636-
throwsUnimplementedError,
710+
await streamQueue.next,
711+
CameraInitializedEvent(
712+
cameraId,
713+
videoSize.width,
714+
videoSize.height,
715+
ExposureMode.auto,
716+
false,
717+
FocusMode.auto,
718+
false,
719+
),
637720
);
721+
722+
await streamQueue.cancel();
638723
});
639724

640725
testWidgets('onCameraResolutionChanged throws UnimplementedError',

packages/camera/camera_web/example/integration_test/helpers/mocks.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
// found in the LICENSE file.
44

55
import 'dart:html';
6+
import 'dart:ui';
67

8+
import 'package:camera_web/src/camera.dart';
79
import 'package:camera_web/src/camera_settings.dart';
810
import 'package:mocktail/mocktail.dart';
911

@@ -17,6 +19,8 @@ class MockCameraSettings extends Mock implements CameraSettings {}
1719

1820
class MockMediaStreamTrack extends Mock implements MediaStreamTrack {}
1921

22+
class MockCamera extends Mock implements Camera {}
23+
2024
/// A fake [MediaStream] that returns the provided [_videoTracks].
2125
class FakeMediaStream extends Fake implements MediaStream {
2226
FakeMediaStream(this._videoTracks);
@@ -54,3 +58,22 @@ class FakeDomException extends Fake implements DomException {
5458
@override
5559
String get name => _name;
5660
}
61+
62+
/// Returns a video element with a blank stream of size [videoSize].
63+
///
64+
/// Can be used to mock a video stream:
65+
/// ```dart
66+
/// final videoElement = getVideoElementWithBlankStream(Size(100, 100));
67+
/// final videoStream = videoElement.captureStream();
68+
/// ```
69+
VideoElement getVideoElementWithBlankStream(Size videoSize) {
70+
final canvasElement = CanvasElement(
71+
width: videoSize.width.toInt(),
72+
height: videoSize.height.toInt(),
73+
)..context2D.fillRect(0, 0, videoSize.width, videoSize.height);
74+
75+
final videoElement = VideoElement()
76+
..srcObject = canvasElement.captureStream();
77+
78+
return videoElement;
79+
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'dart:html' as html;
6+
import 'dart:ui';
67
import 'shims/dart_ui.dart' as ui;
78

89
import 'package:camera_platform_interface/camera_platform_interface.dart';
@@ -171,6 +172,30 @@ class Camera {
171172
return XFile(html.Url.createObjectUrl(blob));
172173
}
173174

175+
/// Returns a size of the camera video based on its first video track size.
176+
///
177+
/// Returns [Size.zero] if the camera is missing a video track or
178+
/// the video track does not include the width or height setting.
179+
Future<Size> getVideoSize() async {
180+
final videoTracks = videoElement.srcObject?.getVideoTracks() ?? [];
181+
182+
if (videoTracks.isEmpty) {
183+
return Size.zero;
184+
}
185+
186+
final defaultVideoTrack = videoTracks.first;
187+
final defaultVideoTrackSettings = defaultVideoTrack.getSettings();
188+
189+
final width = defaultVideoTrackSettings['width'];
190+
final height = defaultVideoTrackSettings['height'];
191+
192+
if (width != null && height != null) {
193+
return Size(width, height);
194+
} else {
195+
return Size.zero;
196+
}
197+
}
198+
174199
/// Disposes the camera by stopping the camera stream
175200
/// and reloading the camera source.
176201
void dispose() {

0 commit comments

Comments
 (0)