Skip to content

Commit e5dceb5

Browse files
authored
[camera] Add camera plugin (#97)
* [camera] Initial commits for camera plugin (#46) * [camera] Initial commit for camera * [camera] Copy an example from original * [camera] Add tizen host for exmple * [camera] Establish camera channel and cameraEvents channel * Add basics to handle method call of camera channel * Add CameraDevice class to represent Camera * [camera] Fix build error * [camera] Implement permission_manager (#51) * Request camera permission when initializing * Add some macros for convenience * Add camera privilege to tizen-manifest.xml * [camera] Migrate to Dart 2.12 and Flutter 2.0 (#55) * [camera] Implement basic behavior of MediaPacketPreviewCb * The preview currently has a fixed orientation, We have been working on how to properly rotate it. * [camera] Migrate to Dart 2.12 and Flutter 2.0 * Revise method handler according to last original camera plugin * Update example * [camera] Add orientation_event_listener (#56) * Add device_method_channel * [camera] Send converted orientation event (#59) * Convert the orientation event according to the lens orientation * Rename orientation_event_listener to manager * Use the prefix 'k' in enumerator names according to the Google Coding Guide * [camera] Implement takePicture (#64) * Flip an Image when using the front camera * Use device orientation * Mimic Android capture file path * [camera] Implement setFocusMode * Return result as a boolean type in camera device API wrapper function * Use the capture file format explicitly as JPEG * [camera] Implement setExposureMode (#70) * Clean-up codes * Fix a crash when Disposing camera * ExposureMode.locked is not supported on TM1 * [camera] Implement setFocusPoint (#71) * Fix build warning * Update minor things * [camera] Send focusPointSupported as a true (#73) * Even if I send this to false when opening camera, the focus point request arrived to method handler normally * [camera] Implement setFlashMode (#74) * Replace if-statement about enumerable with switch * Fix wrong log message * [camera] Implement setZoomLevel (#75) * Implement get{Max|min}ZoomLevel * [camera] Request a recorder permission (#77) * [camera] Use 'Camera' as a wrapper function name prefix * [camera] Request permissions as async callchain * Request a recorder permission when initialize too * [camera] Implement video recording (#79) * Implement {start|stop|pause|resume}Recording * Cleanup code * [camera] Implement lockCaptureOrientation (#81) * [camera] Improve zoom behavior (#82) * Update project files to latest * [camera] Update README (#84) * [camera] Use enable_audio argument (#88) * If enable_audio is false, disable audio track when recording video * [camera] Implement {GetMin|GetMax|Set}Exposure (#92) * [camera] Implement {GetMin|GetMax|Set}Exposure * [camera] Introduce CameraDeviceError * Now, The public API on the CameraDevice throws an error or takes a flutter::MethodResult as an argument to send result * [camera] Update minor things * [camera] Fix a bug related to zoom * Ignore a case where min and max zoom level are the same * [camera] Use LOG_WARN instead of LOG_ERROR * [camera] Migrate example app host to latest * [camera] Update CHANGELOG * Cleaup tidy * Set the version to 0.1.0 * [camera] Sort dependencies in pubspec.yaml * [camera] Implement ResolutionPreset * The resolution values came from resolution_preset.dart This may not be supported by your device. * [camera] Add LICENSE * [camera] Modify camera_test.dart to run on TM1 * This is temporary modifications, please review this again if an official device with camera is released later * [camera] Update GetValueFromEncodableMap tamplate Signed-off-by: Boram Bae <[email protected]>
1 parent 866afb9 commit e5dceb5

31 files changed

+4246
-0
lines changed

packages/camera/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.DS_Store
2+
.dart_tool/
3+
4+
.packages
5+
.pub/
6+
7+
build/

packages/camera/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## 0.1.0
2+
3+
* Initial release.

packages/camera/LICENSE

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Copyright (c) 2021 Samsung Electronics Co., Ltd. All rights reserved.
2+
Copyright (c) 2013 The Flutter Authors. All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without modification,
5+
are permitted provided that the following conditions are met:
6+
7+
* Redistributions of source code must retain the above copyright
8+
notice, this list of conditions and the following disclaimer.
9+
* Redistributions in binary form must reproduce the above
10+
copyright notice, this list of conditions and the following
11+
disclaimer in the documentation and/or other materials provided
12+
with the distribution.
13+
* Neither the names of the copyright holders nor the names of the
14+
contributors may be used to endorse or promote products derived
15+
from this software without specific prior written permission.
16+
17+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
21+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
24+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

packages/camera/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# camera_tizen
2+
3+
The Tizen implementation of [`camera`](https://github.com/flutter/plugins/tree/master/packages/camera).
4+
5+
## Supported devices
6+
7+
This plugin is an experimental plug-in for the future
8+
9+
- Nothing
10+
11+
## Required privileges
12+
13+
To use this plugin, add below lines under the `<manifest>` section in your `tizen-manifest.xml` file,
14+
15+
```xml
16+
<privileges>
17+
<privilege>http://tizen.org/privilege/camera</privilege>
18+
<privilege>http://tizen.org/privilege/recorder</privilege>
19+
</privileges>
20+
```
21+
22+
## Usage
23+
24+
This package is not an _endorsed_ implementation of `camera`. Therefore, you have to include `camera_tizen` alongside `camera` as dependencies in your `pubspec.yaml` file.
25+
26+
```yaml
27+
dependencies:
28+
camera: ^0.8.1
29+
camera_tizen: ^0.1.0
30+
```
31+
32+
Then you can import `camera` in your Dart code:
33+
34+
```dart
35+
import 'package:camera/camera.dart';
36+
```
37+
For detailed usage, see https://github.com/flutter/plugins/tree/master/packages/camera/camera#example.
38+
39+
## Notes
40+
CameraPreview currently does not support other platforms except Android and iOS. Therefor the camera preview to orient properly, you have to modify the `camera_preview.dart`.
41+
42+
```dart
43+
Widget _wrapInRotatedBox({required Widget child}) {
44+
// if (defaultTargetPlatform != TargetPlatform.android) {
45+
// return child;
46+
// }
47+
48+
return RotatedBox(
49+
quarterTurns: _getQuarterTurns(),
50+
child: child,
51+
);
52+
}
53+
```

packages/camera/example/.gitignore

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Miscellaneous
2+
*.class
3+
*.log
4+
*.pyc
5+
*.swp
6+
.DS_Store
7+
.atom/
8+
.buildlog/
9+
.history
10+
.svn/
11+
12+
# IntelliJ related
13+
*.iml
14+
*.ipr
15+
*.iws
16+
.idea/
17+
18+
# The .vscode folder contains launch configuration and tasks you configure in
19+
# VS Code which you may wish to be included in version control, so this line
20+
# is commented out by default.
21+
#.vscode/
22+
23+
# Flutter/Dart/Pub related
24+
**/doc/api/
25+
**/ios/Flutter/.last_build_id
26+
.dart_tool/
27+
.flutter-plugins
28+
.flutter-plugins-dependencies
29+
.packages
30+
.pub-cache/
31+
.pub/
32+
/build/
33+
34+
# Web related
35+
lib/generated_plugin_registrant.dart
36+
37+
# Symbolication related
38+
app.*.symbols
39+
40+
# Obfuscation related
41+
app.*.map.json
42+
43+
# Android Studio will place build artifacts here
44+
/android/app/debug
45+
/android/app/profile
46+
/android/app/release

packages/camera/example/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# camera_tizen_example
2+
3+
Demonstrates how to use the camera_tizen plugin.
4+
5+
## Getting Started
6+
7+
To run this app on your Tizen device, use [flutter-tizen](https://github.com/flutter-tizen/flutter-tizen).
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// TODO(mvanbeusekom): Remove this once flutter_driver supports null safety.
6+
// https://github.com/flutter/flutter/issues/71379
7+
// @dart = 2.9
8+
import 'dart:async';
9+
import 'dart:io';
10+
import 'dart:ui';
11+
12+
import 'package:camera/camera.dart';
13+
import 'package:flutter/painting.dart';
14+
import 'package:flutter_test/flutter_test.dart';
15+
import 'package:path_provider/path_provider.dart';
16+
import 'package:video_player/video_player.dart';
17+
import 'package:integration_test/integration_test.dart';
18+
19+
void main() {
20+
Directory testDir;
21+
22+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
23+
24+
setUpAll(() async {
25+
final Directory extDir = await getTemporaryDirectory();
26+
testDir = await Directory('${extDir.path}/test').create(recursive: true);
27+
});
28+
29+
tearDownAll(() async {
30+
await testDir.delete(recursive: true);
31+
});
32+
33+
// final Map<ResolutionPreset, Size> presetExpectedSizes =
34+
// <ResolutionPreset, Size>{
35+
// ResolutionPreset.low:
36+
// Platform.isAndroid ? const Size(240, 320) : const Size(288, 352),
37+
// ResolutionPreset.medium:
38+
// Platform.isAndroid ? const Size(480, 720) : const Size(480, 640),
39+
// ResolutionPreset.high: const Size(720, 1280),
40+
// ResolutionPreset.veryHigh: const Size(1080, 1920),
41+
// ResolutionPreset.ultraHigh: const Size(2160, 3840),
42+
// // Don't bother checking for max here since it could be anything.
43+
// };
44+
45+
final Map<ResolutionPreset, Size> presetExpectedSizes =
46+
<ResolutionPreset, Size>{
47+
ResolutionPreset.medium: const Size(480, 720),
48+
// Don't bother checking for max here since it could be anything.
49+
};
50+
51+
/// Verify that [actual] has dimensions that are at least as large as
52+
/// [expectedSize]. Allows for a mismatch in portrait vs landscape. Returns
53+
/// whether the dimensions exactly match.
54+
bool assertExpectedDimensions(Size expectedSize, Size actual) {
55+
expect(actual.shortestSide, lessThanOrEqualTo(expectedSize.shortestSide));
56+
expect(actual.longestSide, lessThanOrEqualTo(expectedSize.longestSide));
57+
return actual.shortestSide == expectedSize.shortestSide &&
58+
actual.longestSide == expectedSize.longestSide;
59+
}
60+
61+
// This tests that the capture is no bigger than the preset, since we have
62+
// automatic code to fall back to smaller sizes when we need to. Returns
63+
// whether the image is exactly the desired resolution.
64+
Future<bool> testCaptureImageResolution(
65+
CameraController controller, ResolutionPreset preset) async {
66+
final Size expectedSize = presetExpectedSizes[preset];
67+
print(
68+
'Capturing photo at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}');
69+
70+
// Take Picture
71+
final file = await controller.takePicture();
72+
73+
// Load picture
74+
final File fileImage = File(file.path);
75+
final Image image = await decodeImageFromList(fileImage.readAsBytesSync());
76+
77+
// Verify image dimensions are as expected
78+
expect(image, isNotNull);
79+
return assertExpectedDimensions(
80+
expectedSize, Size(image.height.toDouble(), image.width.toDouble()));
81+
}
82+
83+
testWidgets('Capture specific image resolutions',
84+
(WidgetTester tester) async {
85+
final List<CameraDescription> cameras = await availableCameras();
86+
if (cameras.isEmpty) {
87+
return;
88+
}
89+
for (CameraDescription cameraDescription in cameras) {
90+
bool previousPresetExactlySupported = true;
91+
for (MapEntry<ResolutionPreset, Size> preset
92+
in presetExpectedSizes.entries) {
93+
final CameraController controller =
94+
CameraController(cameraDescription, preset.key);
95+
await controller.initialize();
96+
final bool presetExactlySupported =
97+
await testCaptureImageResolution(controller, preset.key);
98+
assert(!(!previousPresetExactlySupported && presetExactlySupported),
99+
'The camera took higher resolution pictures at a lower resolution.');
100+
previousPresetExactlySupported = presetExactlySupported;
101+
await controller.dispose();
102+
}
103+
}
104+
});
105+
106+
// This tests that the capture is no bigger than the preset, since we have
107+
// automatic code to fall back to smaller sizes when we need to. Returns
108+
// whether the image is exactly the desired resolution.
109+
Future<bool> testCaptureVideoResolution(
110+
CameraController controller, ResolutionPreset preset) async {
111+
final Size expectedSize = presetExpectedSizes[preset];
112+
print(
113+
'Capturing video at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}');
114+
115+
// Take Video
116+
await controller.startVideoRecording();
117+
sleep(const Duration(milliseconds: 300));
118+
final file = await controller.stopVideoRecording();
119+
120+
// Load video metadata
121+
final File videoFile = File(file.path);
122+
final VideoPlayerController videoController =
123+
VideoPlayerController.file(videoFile);
124+
await videoController.initialize();
125+
final Size video = videoController.value.size;
126+
127+
// Verify image dimensions are as expected
128+
expect(video, isNotNull);
129+
return assertExpectedDimensions(
130+
expectedSize, Size(video.height, video.width));
131+
}
132+
133+
testWidgets('Capture specific video resolutions',
134+
(WidgetTester tester) async {
135+
final List<CameraDescription> cameras = await availableCameras();
136+
if (cameras.isEmpty) {
137+
return;
138+
}
139+
for (CameraDescription cameraDescription in cameras) {
140+
bool previousPresetExactlySupported = true;
141+
for (MapEntry<ResolutionPreset, Size> preset
142+
in presetExpectedSizes.entries) {
143+
final CameraController controller =
144+
CameraController(cameraDescription, preset.key);
145+
await controller.initialize();
146+
// await controller.prepareForVideoRecording();
147+
final bool presetExactlySupported =
148+
await testCaptureVideoResolution(controller, preset.key);
149+
assert(!(!previousPresetExactlySupported && presetExactlySupported),
150+
'The camera took higher resolution pictures at a lower resolution.');
151+
previousPresetExactlySupported = presetExactlySupported;
152+
await controller.dispose();
153+
}
154+
}
155+
});
156+
157+
testWidgets('Pause and resume video recording', (WidgetTester tester) async {
158+
final List<CameraDescription> cameras = await availableCameras();
159+
if (cameras.isEmpty) {
160+
return;
161+
}
162+
163+
final CameraController controller = CameraController(
164+
cameras[0],
165+
ResolutionPreset.low,
166+
enableAudio: false,
167+
);
168+
169+
await controller.initialize();
170+
// await controller.prepareForVideoRecording();
171+
172+
int startPause;
173+
int timePaused = 0;
174+
175+
await controller.startVideoRecording();
176+
final int recordingStart = DateTime.now().millisecondsSinceEpoch;
177+
sleep(const Duration(milliseconds: 500));
178+
179+
await controller.pauseVideoRecording();
180+
startPause = DateTime.now().millisecondsSinceEpoch;
181+
sleep(const Duration(milliseconds: 500));
182+
await controller.resumeVideoRecording();
183+
timePaused += DateTime.now().millisecondsSinceEpoch - startPause;
184+
185+
sleep(const Duration(milliseconds: 500));
186+
187+
await controller.pauseVideoRecording();
188+
startPause = DateTime.now().millisecondsSinceEpoch;
189+
sleep(const Duration(milliseconds: 500));
190+
await controller.resumeVideoRecording();
191+
timePaused += DateTime.now().millisecondsSinceEpoch - startPause;
192+
193+
sleep(const Duration(milliseconds: 500));
194+
195+
final file = await controller.stopVideoRecording();
196+
final int recordingTime =
197+
DateTime.now().millisecondsSinceEpoch - recordingStart;
198+
199+
final File videoFile = File(file.path);
200+
final VideoPlayerController videoController = VideoPlayerController.file(
201+
videoFile,
202+
);
203+
await videoController.initialize();
204+
final int duration = videoController.value.duration.inMilliseconds;
205+
await videoController.dispose();
206+
207+
expect(duration, lessThan(recordingTime - timePaused));
208+
});
209+
210+
testWidgets(
211+
'Android image streaming',
212+
(WidgetTester tester) async {
213+
final List<CameraDescription> cameras = await availableCameras();
214+
if (cameras.isEmpty) {
215+
return;
216+
}
217+
218+
final CameraController controller = CameraController(
219+
cameras[0],
220+
ResolutionPreset.low,
221+
enableAudio: false,
222+
);
223+
224+
await controller.initialize();
225+
bool _isDetecting = false;
226+
227+
await controller.startImageStream((CameraImage image) {
228+
if (_isDetecting) return;
229+
230+
_isDetecting = true;
231+
232+
expectLater(image, isNotNull).whenComplete(() => _isDetecting = false);
233+
});
234+
235+
expect(controller.value.isStreamingImages, true);
236+
237+
sleep(const Duration(milliseconds: 500));
238+
239+
await controller.stopImageStream();
240+
await controller.dispose();
241+
},
242+
skip: !Platform.isAndroid,
243+
);
244+
}

0 commit comments

Comments
 (0)