From 1771a18e7066efc0e668ca8578153bd44dfdbb3d Mon Sep 17 00:00:00 2001 From: Swift Kim Date: Thu, 25 Nov 2021 15:48:29 +0900 Subject: [PATCH] [camera] Implement pausePreview and resumePreview (#260) * [camera] Update camera to 0.9.4 * Implement pausePreview and resumePreview --- packages/camera/CHANGELOG.md | 11 +- packages/camera/README.md | 14 +- .../example/integration_test/camera_test.dart | 21 +-- packages/camera/example/lib/main.dart | 134 +++++++++++++----- packages/camera/example/pubspec.yaml | 7 +- .../example/test_driver/integration_test.dart | 67 --------- packages/camera/example/tizen/Runner.csproj | 2 +- .../camera/example/tizen/tizen-manifest.xml | 3 +- packages/camera/pubspec.yaml | 5 +- packages/camera/tizen/src/camera_device.cc | 5 + packages/camera/tizen/src/camera_device.h | 4 + packages/camera/tizen/src/camera_plugin.cc | 6 + 12 files changed, 142 insertions(+), 137 deletions(-) delete mode 100644 packages/camera/example/test_driver/integration_test.dart diff --git a/packages/camera/CHANGELOG.md b/packages/camera/CHANGELOG.md index 78e66bf90..e480c83c3 100644 --- a/packages/camera/CHANGELOG.md +++ b/packages/camera/CHANGELOG.md @@ -4,8 +4,15 @@ ## 0.2.0 -* Apply new external texture APIs +* Apply new external texture APIs. ## 0.2.1 -* Fix a freezing preview issue +* Fix a freezing preview issue. + +## 0.3.0 + +* Update camera to 0.9.4. +* Implement `pausePreview` and `resumePreview`. +* Update the example app and integration_test. +* Remove the unused test driver. diff --git a/packages/camera/README.md b/packages/camera/README.md index d1fd70bc2..12d75a398 100644 --- a/packages/camera/README.md +++ b/packages/camera/README.md @@ -4,13 +4,11 @@ The Tizen implementation of [`camera`](https://github.com/flutter/plugins/tree/m ## Supported devices -This plugin is an experimental plug-in for the future - -- Nothing +This plugin is currently experimental and does not support any devices. ## Required privileges -To use this plugin, add below lines under the `` section in your `tizen-manifest.xml` file, +To use this plugin, add below lines under the `` section in your `tizen-manifest.xml` file. ```xml @@ -25,8 +23,8 @@ This package is not an _endorsed_ implementation of `camera`. Therefore, you hav ```yaml dependencies: - camera: ^0.8.1 - camera_tizen: ^0.2.1 + camera: ^0.9.4 + camera_tizen: ^0.3.0 ``` Then you can import `camera` in your Dart code: @@ -34,10 +32,12 @@ Then you can import `camera` in your Dart code: ```dart import 'package:camera/camera.dart'; ``` + For detailed usage, see https://github.com/flutter/plugins/tree/master/packages/camera/camera#example. ## Notes -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`. + +For the camera preview to rotate correctly, you have to modify the `camera_preview.dart` file as follows. ```dart Widget _wrapInRotatedBox({required Widget child}) { diff --git a/packages/camera/example/integration_test/camera_test.dart b/packages/camera/example/integration_test/camera_test.dart index d4a2dbfdc..8b9571c45 100644 --- a/packages/camera/example/integration_test/camera_test.dart +++ b/packages/camera/example/integration_test/camera_test.dart @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// TODO(mvanbeusekom): Remove this once flutter_driver supports null safety. -// https://github.com/flutter/flutter/issues/71379 -// @dart = 2.9 import 'dart:async'; import 'dart:io'; import 'dart:ui'; @@ -17,7 +14,7 @@ import 'package:video_player/video_player.dart'; import 'package:integration_test/integration_test.dart'; void main() { - Directory testDir; + late Directory testDir; IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -30,18 +27,6 @@ void main() { await testDir.delete(recursive: true); }); - // final Map presetExpectedSizes = - // { - // 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. - // }; - final Map presetExpectedSizes = { ResolutionPreset.medium: const Size(480, 720), @@ -63,7 +48,7 @@ void main() { // whether the image is exactly the desired resolution. Future testCaptureImageResolution( CameraController controller, ResolutionPreset preset) async { - final Size expectedSize = presetExpectedSizes[preset]; + final Size expectedSize = presetExpectedSizes[preset]!; print( 'Capturing photo at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}'); @@ -108,7 +93,7 @@ void main() { // whether the image is exactly the desired resolution. Future testCaptureVideoResolution( CameraController controller, ResolutionPreset preset) async { - final Size expectedSize = presetExpectedSizes[preset]; + final Size expectedSize = presetExpectedSizes[preset]!; print( 'Capturing video at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}'); diff --git a/packages/camera/example/lib/main.dart b/packages/camera/example/lib/main.dart index 536e95de9..a3a5d1d46 100644 --- a/packages/camera/example/lib/main.dart +++ b/packages/camera/example/lib/main.dart @@ -8,6 +8,7 @@ import 'dart:async'; import 'dart:io'; import 'package:camera/camera.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; @@ -68,7 +69,7 @@ class _CameraExampleHomeState extends State @override void initState() { super.initState(); - WidgetsBinding.instance?.addObserver(this); + _ambiguate(WidgetsBinding.instance)?.addObserver(this); _flashModeControlRowAnimationController = AnimationController( duration: const Duration(milliseconds: 300), @@ -98,7 +99,7 @@ class _CameraExampleHomeState extends State @override void dispose() { - WidgetsBinding.instance?.removeObserver(this); + _ambiguate(WidgetsBinding.instance)?.removeObserver(this); _flashModeControlRowAnimationController.dispose(); _exposureModeControlRowAnimationController.dispose(); super.dispose(); @@ -231,7 +232,14 @@ class _CameraExampleHomeState extends State ? Container() : SizedBox( child: (localVideoController == null) - ? Image.file(File(imageFile!.path)) + ? ( + // The captured image on the web contains a network-accessible URL + // pointing to a location within the browser. It may be displayed + // either with Image.network or Image.memory after loading the image + // bytes to memory. + kIsWeb + ? Image.network(imageFile!.path) + : Image.file(File(imageFile!.path))) : Container( child: Center( child: AspectRatio( @@ -267,17 +275,24 @@ class _CameraExampleHomeState extends State color: Colors.blue, onPressed: controller != null ? onFlashModeButtonPressed : null, ), - IconButton( - icon: Icon(Icons.exposure), - color: Colors.blue, - onPressed: - controller != null ? onExposureModeButtonPressed : null, - ), - IconButton( - icon: Icon(Icons.filter_center_focus), - color: Colors.blue, - onPressed: controller != null ? onFocusModeButtonPressed : null, - ), + // The exposure and focus mode are currently not supported on the web. + ...(!kIsWeb + ? [ + IconButton( + icon: Icon(Icons.exposure), + color: Colors.blue, + onPressed: controller != null + ? onExposureModeButtonPressed + : null, + ), + IconButton( + icon: Icon(Icons.filter_center_focus), + color: Colors.blue, + onPressed: + controller != null ? onFocusModeButtonPressed : null, + ) + ] + : []), IconButton( icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute), color: Colors.blue, @@ -399,6 +414,13 @@ class _CameraExampleHomeState extends State onSetExposureModeButtonPressed(ExposureMode.locked) : null, ), + TextButton( + child: Text('RESET OFFSET'), + style: styleLocked, + onPressed: controller != null + ? () => controller!.setExposureOffset(0.0) + : null, + ), ], ), Center( @@ -530,7 +552,16 @@ class _CameraExampleHomeState extends State cameraController.value.isRecordingVideo ? onStopButtonPressed : null, - ) + ), + IconButton( + icon: const Icon(Icons.pause_presentation), + color: + cameraController != null && cameraController.value.isPreviewPaused + ? Colors.red + : Colors.blue, + onPressed: + cameraController == null ? null : onPausePreviewButtonPressed, + ), ], ); } @@ -597,12 +628,14 @@ class _CameraExampleHomeState extends State if (controller != null) { await controller!.dispose(); } + final CameraController cameraController = CameraController( cameraDescription, - ResolutionPreset.medium, + kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium, enableAudio: enableAudio, imageFormatGroup: ImageFormatGroup.jpeg, ); + controller = cameraController; // If the controller is updated then update the UI. @@ -617,12 +650,17 @@ class _CameraExampleHomeState extends State try { await cameraController.initialize(); await Future.wait([ - cameraController - .getMinExposureOffset() - .then((value) => _minAvailableExposureOffset = value), - cameraController - .getMaxExposureOffset() - .then((value) => _maxAvailableExposureOffset = value), + // The exposure mode is currently not supported on the web. + ...(!kIsWeb + ? [ + cameraController + .getMinExposureOffset() + .then((value) => _minAvailableExposureOffset = value), + cameraController + .getMaxExposureOffset() + .then((value) => _maxAvailableExposureOffset = value) + ] + : []), cameraController .getMaxZoomLevel() .then((value) => _maxAvailableZoom = value), @@ -690,16 +728,20 @@ class _CameraExampleHomeState extends State } void onCaptureOrientationLockButtonPressed() async { - if (controller != null) { - final CameraController cameraController = controller!; - if (cameraController.value.isCaptureOrientationLocked) { - await cameraController.unlockCaptureOrientation(); - showInSnackBar('Capture orientation unlocked'); - } else { - await cameraController.lockCaptureOrientation(); - showInSnackBar( - 'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}'); + try { + if (controller != null) { + final CameraController cameraController = controller!; + if (cameraController.value.isCaptureOrientationLocked) { + await cameraController.unlockCaptureOrientation(); + showInSnackBar('Capture orientation unlocked'); + } else { + await cameraController.lockCaptureOrientation(); + showInSnackBar( + 'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}'); + } } + } on CameraException catch (e) { + _showCameraException(e); } } @@ -741,6 +783,23 @@ class _CameraExampleHomeState extends State }); } + Future onPausePreviewButtonPressed() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return; + } + + if (cameraController.value.isPreviewPaused) { + await cameraController.resumePreview(); + } else { + await cameraController.pausePreview(); + } + + if (mounted) setState(() {}); + } + void onPauseButtonPressed() { pauseVideoRecording().then((_) { if (mounted) setState(() {}); @@ -881,8 +940,10 @@ class _CameraExampleHomeState extends State return; } - final VideoPlayerController vController = - VideoPlayerController.file(File(videoFile!.path)); + final VideoPlayerController vController = kIsWeb + ? VideoPlayerController.network(videoFile!.path) + : VideoPlayerController.file(File(videoFile!.path)); + videoPlayerListener = () { if (videoController != null && videoController!.value.size != null) { // Refreshing the state to update video player with the correct ratio. @@ -951,3 +1012,10 @@ Future main() async { } runApp(CameraApp()); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +// TODO(ianh): Remove this once we roll stable in late 2021. +T? _ambiguate(T? value) => value; diff --git a/packages/camera/example/pubspec.yaml b/packages/camera/example/pubspec.yaml index 78c487e5b..4ba3c2dc4 100644 --- a/packages/camera/example/pubspec.yaml +++ b/packages/camera/example/pubspec.yaml @@ -1,8 +1,9 @@ name: camera_example description: Demonstrates how to use the camera plugin. +publish_to: "none" dependencies: - camera: ^0.8.1 + camera: ^0.9.4 camera_tizen: path: ../ flutter: @@ -10,13 +11,11 @@ dependencies: path_provider: ^2.0.0 path_provider_tizen: path: ../../path_provider - video_player: ^2.0.0 + video_player: ^2.1.4 video_player_tizen: path: ../../video_player dev_dependencies: - flutter_driver: - sdk: flutter flutter_test: sdk: flutter integration_test: diff --git a/packages/camera/example/test_driver/integration_test.dart b/packages/camera/example/test_driver/integration_test.dart deleted file mode 100644 index fac399066..000000000 --- a/packages/camera/example/test_driver/integration_test.dart +++ /dev/null @@ -1,67 +0,0 @@ -// 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. - -// TODO(mvanbeusekom): Remove this once flutter_driver supports null safety. -// https://github.com/flutter/flutter/issues/71379 -// @dart = 2.9 -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter_driver/flutter_driver.dart'; - -const String _examplePackage = 'io.flutter.plugins.cameraexample'; - -Future main() async { - if (!(Platform.isLinux || Platform.isMacOS)) { - print('This test must be run on a POSIX host. Skipping...'); - exit(0); - } - final bool adbExists = - Process.runSync('which', ['adb']).exitCode == 0; - if (!adbExists) { - print('This test needs ADB to exist on the \$PATH. Skipping...'); - exit(0); - } - print('Granting camera permissions...'); - Process.runSync('adb', [ - 'shell', - 'pm', - 'grant', - _examplePackage, - 'android.permission.CAMERA' - ]); - Process.runSync('adb', [ - 'shell', - 'pm', - 'grant', - _examplePackage, - 'android.permission.RECORD_AUDIO' - ]); - print('Starting test.'); - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = await driver.requestData( - null, - timeout: const Duration(minutes: 1), - ); - await driver.close(); - print('Test finished. Revoking camera permissions...'); - Process.runSync('adb', [ - 'shell', - 'pm', - 'revoke', - _examplePackage, - 'android.permission.CAMERA' - ]); - Process.runSync('adb', [ - 'shell', - 'pm', - 'revoke', - _examplePackage, - 'android.permission.RECORD_AUDIO' - ]); - - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} diff --git a/packages/camera/example/tizen/Runner.csproj b/packages/camera/example/tizen/Runner.csproj index f8346a628..c3c43aed9 100644 --- a/packages/camera/example/tizen/Runner.csproj +++ b/packages/camera/example/tizen/Runner.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/packages/camera/example/tizen/tizen-manifest.xml b/packages/camera/example/tizen/tizen-manifest.xml index fddccc408..feeb805cb 100644 --- a/packages/camera/example/tizen/tizen-manifest.xml +++ b/packages/camera/example/tizen/tizen-manifest.xml @@ -1,11 +1,10 @@ - + ic_launcher.png - diff --git a/packages/camera/pubspec.yaml b/packages/camera/pubspec.yaml index 8f2b5eb95..85b79c161 100644 --- a/packages/camera/pubspec.yaml +++ b/packages/camera/pubspec.yaml @@ -2,15 +2,14 @@ name: camera_tizen description: Tizen implementation of the camera plugin homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/camera -version: 0.2.1 +version: 0.3.0 dependencies: + camera_platform_interface: ^2.1.1 flutter: sdk: flutter dev_dependencies: - flutter_test: - sdk: flutter pedantic: ^1.10.0 flutter: diff --git a/packages/camera/tizen/src/camera_device.cc b/packages/camera/tizen/src/camera_device.cc index 2e2738ac4..edd736cc4 100644 --- a/packages/camera/tizen/src/camera_device.cc +++ b/packages/camera/tizen/src/camera_device.cc @@ -993,6 +993,11 @@ void CameraDevice::Open( self->prepared_packet_ = packet; return; } + if (self->is_preview_paused_) { + media_packet_destroy(packet); + self->prepared_packet_ = nullptr; + return; + } self->prepared_packet_ = packet; self->registrar_->texture_registrar()->MarkTextureFrameAvailable( self->texture_id_); diff --git a/packages/camera/tizen/src/camera_device.h b/packages/camera/tizen/src/camera_device.h index 4e7e3c5f0..aa4168861 100644 --- a/packages/camera/tizen/src/camera_device.h +++ b/packages/camera/tizen/src/camera_device.h @@ -250,6 +250,9 @@ class CameraDevice { void LockCaptureOrientation(OrientationType orientation); void UnlockCaptureOrientation(); + void PausePreview() { is_preview_paused_ = true; } + void ResumePreview() { is_preview_paused_ = false; } + private: bool CreateCamera(); bool ClearCameraAutoFocusArea(); @@ -357,6 +360,7 @@ class CameraDevice { std::vector> supported_recorder_resolutions_; bool enable_audio_{true}; + bool is_preview_paused_{false}; }; #endif diff --git a/packages/camera/tizen/src/camera_plugin.cc b/packages/camera/tizen/src/camera_plugin.cc index d4dcd2c1b..035b72b57 100644 --- a/packages/camera/tizen/src/camera_plugin.cc +++ b/packages/camera/tizen/src/camera_plugin.cc @@ -301,6 +301,12 @@ class CameraPlugin : public flutter::Plugin { } else if (method_name == "unlockCaptureOrientation") { camera_->UnlockCaptureOrientation(); result->Success(); + } else if (method_name == "pausePreview") { + camera_->PausePreview(); + result->Success(); + } else if (method_name == "resumePreview") { + camera_->ResumePreview(); + result->Success(); } else if (method_name == "dispose") { if (camera_) { camera_->Dispose();