Skip to content

Commit

Permalink
[camera_web] Add initializeCamera implementation (flutter#4186)
Browse files Browse the repository at this point in the history
  • Loading branch information
bselwe authored Jul 24, 2021
1 parent 0f26618 commit 400fa20
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

import 'dart:html';
import 'dart:ui';

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

final videoElement = VideoElement()
..src =
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'
..preload = 'true'
..width = 10
..height = 10;

final videoElement = getVideoElementWithBlankStream(Size(10, 10));
mediaStream = videoElement.captureStream();

when(() => window.navigator).thenReturn(navigator);
Expand Down Expand Up @@ -469,6 +464,49 @@ void main() {
});
});

group('getVideoSize', () {
testWidgets(
'returns a size '
'based on the first video track settings', (tester) async {
const videoSize = Size(1280, 720);

final videoElement = getVideoElementWithBlankStream(videoSize);
mediaStream = videoElement.captureStream();

final camera = Camera(
textureId: 1,
window: window,
);

await camera.initialize();

expect(
await camera.getVideoSize(),
equals(videoSize),
);
});

testWidgets(
'returns Size.zero '
'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: 1,
window: window,
);

await camera.initialize();

expect(
await camera.getVideoSize(),
equals(Size.zero),
);
});
});

group('dispose', () {
testWidgets('resets the video element\'s source', (tester) async {
final camera = Camera(
Expand Down
143 changes: 114 additions & 29 deletions packages/camera/camera_web/example/integration_test/camera_web_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'dart:html';
import 'dart:ui';

import 'package:async/async.dart';
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:camera_web/camera_web.dart';
import 'package:camera_web/src/camera.dart';
Expand Down Expand Up @@ -33,13 +34,8 @@ void main() {
window = MockWindow();
navigator = MockNavigator();
mediaDevices = MockMediaDevices();
videoElement = VideoElement()
..src =
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'
..preload = 'true'
..width = 10
..height = 10
..crossOrigin = 'anonymous';

videoElement = getVideoElementWithBlankStream(Size(10, 10));

cameraSettings = MockCameraSettings();

Expand Down Expand Up @@ -327,21 +323,18 @@ void main() {
const ultraHighResolutionSize = Size(3840, 2160);
const maxResolutionSize = Size(3840, 2160);

late CameraDescription cameraDescription;
late CameraMetadata cameraMetadata;

setUp(() {
cameraDescription = CameraDescription(
name: 'name',
lensDirection: CameraLensDirection.front,
sensorOrientation: 0,
);
final cameraDescription = CameraDescription(
name: 'name',
lensDirection: CameraLensDirection.front,
sensorOrientation: 0,
);

cameraMetadata = CameraMetadata(
deviceId: 'deviceId',
facingMode: 'user',
);
final cameraMetadata = CameraMetadata(
deviceId: 'deviceId',
facingMode: 'user',
);

setUp(() {
// Add metadata for the camera description.
(CameraPlatform.instance as CameraPlugin)
.camerasMetadata[cameraDescription] = cameraMetadata;
Expand Down Expand Up @@ -434,11 +427,38 @@ void main() {
});
});

testWidgets('initializeCamera throws UnimplementedError', (tester) async {
expect(
() => CameraPlatform.instance.initializeCamera(cameraId),
throwsUnimplementedError,
);
group('initializeCamera', () {
testWidgets(
'throws CameraException '
'with notFound error '
'if the camera does not exist', (tester) async {
expect(
() => CameraPlatform.instance.initializeCamera(cameraId),
throwsA(
isA<CameraException>().having(
(e) => e.code,
'code',
CameraErrorCodes.notFound,
),
),
);
});

testWidgets('initializes and plays the camera', (tester) async {
final camera = MockCamera();

when(camera.getVideoSize).thenAnswer((_) => Future.value(Size(10, 10)));
when(camera.initialize).thenAnswer((_) => Future.value());
when(camera.play).thenAnswer((_) => Future.value());

// Save the camera in the camera plugin.
(CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera;

await CameraPlatform.instance.initializeCamera(cameraId);

verify(camera.initialize).called(1);
verify(camera.play).called(1);
});
});

testWidgets('lockCaptureOrientation throws UnimplementedError',
Expand Down Expand Up @@ -628,13 +648,78 @@ void main() {
);
});

group('getCamera', () {
testWidgets('returns the correct camera', (tester) async {
final camera = Camera(textureId: cameraId, window: window);

// Save the camera in the camera plugin.
(CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera;

expect(
(CameraPlatform.instance as CameraPlugin).getCamera(cameraId),
equals(camera),
);
});

testWidgets(
'throws CameraException '
'with notFound error '
'if the camera does not exist', (tester) async {
expect(
() => (CameraPlatform.instance as CameraPlugin).getCamera(cameraId),
throwsA(
isA<CameraException>().having(
(e) => e.code,
'code',
CameraErrorCodes.notFound,
),
),
);
});
});

group('events', () {
testWidgets('onCameraInitialized throws UnimplementedError',
(tester) async {
testWidgets(
'onCameraInitialized emits a CameraInitializedEvent '
'on initializeCamera', (tester) async {
// Mock the camera to use a blank video stream of size 1280x720.
const videoSize = Size(1280, 720);

videoElement = getVideoElementWithBlankStream(videoSize);

when(
() => mediaDevices.getUserMedia(any()),
).thenAnswer((_) async => videoElement.captureStream());

final camera = Camera(
textureId: cameraId,
window: window,
);

// Save the camera in the camera plugin.
(CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera;

final Stream<CameraInitializedEvent> eventStream =
CameraPlatform.instance.onCameraInitialized(cameraId);

final streamQueue = StreamQueue(eventStream);

await CameraPlatform.instance.initializeCamera(cameraId);

expect(
() => CameraPlatform.instance.onCameraInitialized(cameraId),
throwsUnimplementedError,
await streamQueue.next,
CameraInitializedEvent(
cameraId,
videoSize.width,
videoSize.height,
ExposureMode.auto,
false,
FocusMode.auto,
false,
),
);

await streamQueue.cancel();
});

testWidgets('onCameraResolutionChanged throws UnimplementedError',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
// found in the LICENSE file.

import 'dart:html';
import 'dart:ui';

import 'package:camera_web/src/camera.dart';
import 'package:camera_web/src/camera_settings.dart';
import 'package:mocktail/mocktail.dart';

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

class MockMediaStreamTrack extends Mock implements MediaStreamTrack {}

class MockCamera extends Mock implements Camera {}

/// A fake [MediaStream] that returns the provided [_videoTracks].
class FakeMediaStream extends Fake implements MediaStream {
FakeMediaStream(this._videoTracks);
Expand Down Expand Up @@ -54,3 +58,22 @@ class FakeDomException extends Fake implements DomException {
@override
String get name => _name;
}

/// Returns a video element with a blank stream of size [videoSize].
///
/// Can be used to mock a video stream:
/// ```dart
/// final videoElement = getVideoElementWithBlankStream(Size(100, 100));
/// final videoStream = videoElement.captureStream();
/// ```
VideoElement getVideoElementWithBlankStream(Size videoSize) {
final canvasElement = CanvasElement(
width: videoSize.width.toInt(),
height: videoSize.height.toInt(),
)..context2D.fillRect(0, 0, videoSize.width, videoSize.height);

final videoElement = VideoElement()
..srcObject = canvasElement.captureStream();

return videoElement;
}
25 changes: 25 additions & 0 deletions packages/camera/camera_web/lib/src/camera.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

import 'dart:html' as html;
import 'dart:ui';
import 'shims/dart_ui.dart' as ui;

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

/// Returns a size of the camera video based on its first video track size.
///
/// Returns [Size.zero] if the camera is missing a video track or
/// the video track does not include the width or height setting.
Future<Size> getVideoSize() async {
final videoTracks = videoElement.srcObject?.getVideoTracks() ?? [];

if (videoTracks.isEmpty) {
return Size.zero;
}

final defaultVideoTrack = videoTracks.first;
final defaultVideoTrackSettings = defaultVideoTrack.getSettings();

final width = defaultVideoTrackSettings['width'];
final height = defaultVideoTrackSettings['height'];

if (width != null && height != null) {
return Size(width, height);
} else {
return Size.zero;
}
}

/// Disposes the camera by stopping the camera stream
/// and reloading the camera source.
void dispose() {
Expand Down
Loading

0 comments on commit 400fa20

Please sign in to comment.