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

[camera_web] Add initializeCamera implementation #4186

Merged
merged 7 commits into from
Jul 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heck yes, this is great!

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