blob: 3af291afe63b00ef2fab9c0bdecdc5fc0cc39faa [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:camera/camera.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path_provider/path_provider.dart';
import 'package:video_player/video_player.dart';
import 'package:integration_test/integration_test.dart';
void main() {
late Directory testDir;
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() async {
final Directory extDir = await getTemporaryDirectory();
testDir = await Directory('${extDir.path}/test').create(recursive: true);
});
tearDownAll(() async {
await testDir.delete(recursive: true);
});
final Map<ResolutionPreset, Size> presetExpectedSizes =
<ResolutionPreset, Size>{
ResolutionPreset.low:
Platform.isAndroid ? const Size(240, 320) : const Size(288, 352),
ResolutionPreset.medium:
Platform.isAndroid ? const Size(480, 720) : const Size(480, 640),
ResolutionPreset.high: const Size(720, 1280),
ResolutionPreset.veryHigh: const Size(1080, 1920),
ResolutionPreset.ultraHigh: const Size(2160, 3840),
// Don't bother checking for max here since it could be anything.
};
/// Verify that [actual] has dimensions that are at least as large as
/// [expectedSize]. Allows for a mismatch in portrait vs landscape. Returns
/// whether the dimensions exactly match.
bool assertExpectedDimensions(Size expectedSize, Size actual) {
expect(actual.shortestSide, lessThanOrEqualTo(expectedSize.shortestSide));
expect(actual.longestSide, lessThanOrEqualTo(expectedSize.longestSide));
return actual.shortestSide == expectedSize.shortestSide &&
actual.longestSide == expectedSize.longestSide;
}
// This tests that the capture is no bigger than the preset, since we have
// automatic code to fall back to smaller sizes when we need to. Returns
// whether the image is exactly the desired resolution.
Future<bool> testCaptureImageResolution(
CameraController controller, ResolutionPreset preset) async {
final Size expectedSize = presetExpectedSizes[preset]!;
print(
'Capturing photo at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}');
// Take Picture
final file = await controller.takePicture();
// Load picture
final File fileImage = File(file.path);
final Image image = await decodeImageFromList(fileImage.readAsBytesSync());
// Verify image dimensions are as expected
expect(image, isNotNull);
return assertExpectedDimensions(
expectedSize, Size(image.height.toDouble(), image.width.toDouble()));
}
testWidgets(
'Capture specific image resolutions',
(WidgetTester tester) async {
final List<CameraDescription> cameras = await availableCameras();
if (cameras.isEmpty) {
return;
}
for (CameraDescription cameraDescription in cameras) {
bool previousPresetExactlySupported = true;
for (MapEntry<ResolutionPreset, Size> preset
in presetExpectedSizes.entries) {
final CameraController controller =
CameraController(cameraDescription, preset.key);
await controller.initialize();
final bool presetExactlySupported =
await testCaptureImageResolution(controller, preset.key);
assert(!(!previousPresetExactlySupported && presetExactlySupported),
'The camera took higher resolution pictures at a lower resolution.');
previousPresetExactlySupported = presetExactlySupported;
await controller.dispose();
}
}
},
// TODO(egarciad): Fix https://github.com/flutter/flutter/issues/93686.
skip: true,
);
// This tests that the capture is no bigger than the preset, since we have
// automatic code to fall back to smaller sizes when we need to. Returns
// whether the image is exactly the desired resolution.
Future<bool> testCaptureVideoResolution(
CameraController controller, ResolutionPreset preset) async {
final Size expectedSize = presetExpectedSizes[preset]!;
print(
'Capturing video at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}');
// Take Video
await controller.startVideoRecording();
sleep(const Duration(milliseconds: 300));
final file = await controller.stopVideoRecording();
// Load video metadata
final File videoFile = File(file.path);
final VideoPlayerController videoController =
VideoPlayerController.file(videoFile);
await videoController.initialize();
final Size video = videoController.value.size;
// Verify image dimensions are as expected
expect(video, isNotNull);
return assertExpectedDimensions(
expectedSize, Size(video.height, video.width));
}
testWidgets(
'Capture specific video resolutions',
(WidgetTester tester) async {
final List<CameraDescription> cameras = await availableCameras();
if (cameras.isEmpty) {
return;
}
for (CameraDescription cameraDescription in cameras) {
bool previousPresetExactlySupported = true;
for (MapEntry<ResolutionPreset, Size> preset
in presetExpectedSizes.entries) {
final CameraController controller =
CameraController(cameraDescription, preset.key);
await controller.initialize();
await controller.prepareForVideoRecording();
final bool presetExactlySupported =
await testCaptureVideoResolution(controller, preset.key);
assert(!(!previousPresetExactlySupported && presetExactlySupported),
'The camera took higher resolution pictures at a lower resolution.');
previousPresetExactlySupported = presetExactlySupported;
await controller.dispose();
}
}
},
// TODO(egarciad): Fix https://github.com/flutter/flutter/issues/93686.
skip: true,
);
testWidgets('Pause and resume video recording', (WidgetTester tester) async {
final List<CameraDescription> cameras = await availableCameras();
if (cameras.isEmpty) {
return;
}
final CameraController controller = CameraController(
cameras[0],
ResolutionPreset.low,
enableAudio: false,
);
await controller.initialize();
await controller.prepareForVideoRecording();
int startPause;
int timePaused = 0;
await controller.startVideoRecording();
final int recordingStart = DateTime.now().millisecondsSinceEpoch;
sleep(const Duration(milliseconds: 500));
await controller.pauseVideoRecording();
startPause = DateTime.now().millisecondsSinceEpoch;
sleep(const Duration(milliseconds: 500));
await controller.resumeVideoRecording();
timePaused += DateTime.now().millisecondsSinceEpoch - startPause;
sleep(const Duration(milliseconds: 500));
await controller.pauseVideoRecording();
startPause = DateTime.now().millisecondsSinceEpoch;
sleep(const Duration(milliseconds: 500));
await controller.resumeVideoRecording();
timePaused += DateTime.now().millisecondsSinceEpoch - startPause;
sleep(const Duration(milliseconds: 500));
final file = await controller.stopVideoRecording();
final int recordingTime =
DateTime.now().millisecondsSinceEpoch - recordingStart;
final File videoFile = File(file.path);
final VideoPlayerController videoController = VideoPlayerController.file(
videoFile,
);
await videoController.initialize();
final int duration = videoController.value.duration.inMilliseconds;
await videoController.dispose();
expect(duration, lessThan(recordingTime - timePaused));
}, skip: !Platform.isAndroid);
testWidgets(
'Android image streaming',
(WidgetTester tester) async {
final List<CameraDescription> cameras = await availableCameras();
if (cameras.isEmpty) {
return;
}
final CameraController controller = CameraController(
cameras[0],
ResolutionPreset.low,
enableAudio: false,
);
await controller.initialize();
bool _isDetecting = false;
await controller.startImageStream((CameraImage image) {
if (_isDetecting) return;
_isDetecting = true;
expectLater(image, isNotNull).whenComplete(() => _isDetecting = false);
});
expect(controller.value.isStreamingImages, true);
sleep(const Duration(milliseconds: 500));
await controller.stopImageStream();
await controller.dispose();
},
skip: !Platform.isAndroid,
);
/// Start streaming with specifying the ImageFormatGroup.
Future<CameraImage> startStreaming(List<CameraDescription> cameras,
ImageFormatGroup? imageFormatGroup) async {
final CameraController controller = CameraController(
cameras.first,
ResolutionPreset.low,
enableAudio: false,
imageFormatGroup: imageFormatGroup,
);
await controller.initialize();
final _completer = Completer<CameraImage>();
await controller.startImageStream((CameraImage image) {
if (!_completer.isCompleted) {
Future(() async {
await controller.stopImageStream();
await controller.dispose();
}).then((value) {
_completer.complete(image);
});
}
});
return _completer.future;
}
testWidgets(
'iOS image streaming with imageFormatGroup',
(WidgetTester tester) async {
final List<CameraDescription> cameras = await availableCameras();
if (cameras.isEmpty) {
return;
}
var _image = await startStreaming(cameras, null);
expect(_image, isNotNull);
expect(_image.format.group, ImageFormatGroup.bgra8888);
expect(_image.planes.length, 1);
_image = await startStreaming(cameras, ImageFormatGroup.yuv420);
expect(_image, isNotNull);
expect(_image.format.group, ImageFormatGroup.yuv420);
expect(_image.planes.length, 2);
_image = await startStreaming(cameras, ImageFormatGroup.bgra8888);
expect(_image, isNotNull);
expect(_image.format.group, ImageFormatGroup.bgra8888);
expect(_image.planes.length, 1);
},
skip: !Platform.isIOS,
);
}