diff --git a/packages/camera/lib/new/src/camera_controller.dart b/packages/camera/lib/new/src/camera_controller.dart index 4ae6b472a3a7..4296f39d7002 100644 --- a/packages/camera/lib/new/src/camera_controller.dart +++ b/packages/camera/lib/new/src/camera_controller.dart @@ -12,14 +12,21 @@ import 'common/camera_interface.dart'; /// /// Use [CameraController.availableCameras] to get a list of available cameras. /// -/// This class is used as a simple interface that works for Android and iOS. +/// This class is used as a simple interface to control a camera on Android or +/// iOS. /// -/// When using iOS, simultaneously calling [start] on two [CameraController]s -/// will throw a [PlatformException]. +/// Only one instance of [CameraController] can be active at a time. If you call +/// [initialize] on a [CameraController] while another is active, the old +/// controller will be disposed before initializing the new controller. /// -/// When using Android, simultaneously calling [start] on two -/// [CameraController]s may throw a [PlatformException] depending on the -/// hardware resources of the device. +/// Example using [CameraController]: +/// +/// ```dart +/// final List cameras = async CameraController.availableCameras(); +/// final CameraController controller = CameraController(description: cameras[0]); +/// controller.initialize(); +/// controller.start(); +/// ``` class CameraController { /// Default constructor. /// @@ -28,7 +35,6 @@ class CameraController { /// /// This will choose the best [CameraConfigurator] for the current device. factory CameraController({@required CameraDescription description}) { - assert(description != null); return CameraController._( description: description, configurator: _createDefaultConfigurator(description), @@ -59,6 +65,14 @@ class CameraController { ); } + static const String _isNotInitializedMessage = 'Initialize was not called.'; + static const String _isDisposedMessage = 'This controller has been disposed.'; + + // Keep only one active instance of CameraController. + static CameraController _instance; + + bool _isDisposed = false; + /// Details for the camera this controller accesses. final CameraDescription description; @@ -68,6 +82,8 @@ class CameraController { /// Api used by the [configurator]. final CameraApi api; + bool get isDisposed => _isDisposed; + /// Retrieves a list of available cameras for the current device. /// /// This will choose the best [CameraAPI] for the current device. @@ -75,14 +91,54 @@ class CameraController { throw UnimplementedError('$defaultTargetPlatform not supported'); } - /// Begins the flow of data between the inputs and outputs connected the camera instance. - Future start() => configurator.start(); + /// Initializes the camera on the device. + /// + /// You must call [dispose] when you are done using the camera, otherwise it + /// will remain locked and be unavailable to other applications. + /// + /// Only one instance of [CameraController] can be active at a time. If you + /// call [initialize] on a [CameraController] while another is active, the old + /// controller will be disposed before initializing the new controller. + Future initialize() { + if (_instance == this) { + return Future.value(); + } + + final Completer completer = Completer(); + + if (_instance != null) { + _instance + .dispose() + .then((_) => configurator.initialize()) + .then((_) => completer.complete()); + } + _instance = this; + + return completer.future; + } + + /// Begins the flow of data between the inputs and outputs connected to the camera instance. + Future start() { + assert(!_isDisposed, _isDisposedMessage); + assert(_instance != this, _isNotInitializedMessage); + + return configurator.start(); + } + + /// Stops the flow of data between the inputs and outputs connected to the camera instance. + Future stop() { + assert(!_isDisposed, _isDisposedMessage); + assert(_instance != this, _isNotInitializedMessage); - /// Stops the flow of data between the inputs and outputs connected the camera instance. - Future stop() => configurator.stop(); + return configurator.stop(); + } /// Deallocate all resources and disables further use of the controller. - Future dispose() => configurator.dispose(); + Future dispose() { + _instance = null; + _isDisposed = true; + return configurator.dispose(); + } static CameraConfigurator _createDefaultConfigurator( CameraDescription description, @@ -101,10 +157,15 @@ class CameraController { } static CameraApi _getCameraApi(CameraDescription description) { + return CameraApi.iOS; + + // TODO(bparrishMines): Uncomment this when platform specific code is added. + /* throw ArgumentError.value( description.runtimeType, 'description.runtimeType', 'Failed to get $CameraApi from', ); + */ } } diff --git a/packages/camera/lib/new/src/common/camera_interface.dart b/packages/camera/lib/new/src/common/camera_interface.dart index 8b5ca9d4e4a3..99ead09550c9 100644 --- a/packages/camera/lib/new/src/common/camera_interface.dart +++ b/packages/camera/lib/new/src/common/camera_interface.dart @@ -38,12 +38,15 @@ abstract class CameraConfigurator { /// You must call [addPreviewTexture] first or this will only return null. int get previewTextureId; - /// Begins the flow of data between the inputs and outputs connected the camera instance. + /// Initializes the camera on the device. + Future initialize(); + + /// Begins the flow of data between the inputs and outputs connected to the camera instance. /// /// This will start updating the texture with id: [previewTextureId]. Future start(); - /// Stops the flow of data between the inputs and outputs connected the camera instance. + /// Stops the flow of data between the inputs and outputs connected to the camera instance. Future stop(); /// Dispose all resources and disables further use of this configurator. diff --git a/packages/camera/test/camera_test.dart b/packages/camera/test/camera_test.dart index ae6c38f9b1f9..39b08ecd8020 100644 --- a/packages/camera/test/camera_test.dart +++ b/packages/camera/test/camera_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:camera/new/camera.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:camera/new/src/camera_testing.dart'; @@ -33,6 +34,41 @@ void main() { CameraTesting.nextHandle = 0; }); + group('$CameraController', () { + test('Initializing a second controller closes the first', () { + final MockCameraDescription description = MockCameraDescription(); + final MockCameraConfigurator configurator = MockCameraConfigurator(); + + final CameraController controller1 = + CameraController.customConfigurator( + description: description, + configurator: configurator, + ); + + controller1.initialize(); + + final CameraController controller2 = + CameraController.customConfigurator( + description: description, + configurator: configurator, + ); + + controller2.initialize(); + + expect( + () => controller1.start(), + throwsA(isInstanceOf()), + ); + + expect( + () => controller1.stop(), + throwsA(isInstanceOf()), + ); + + expect(controller1.isDisposed, isTrue); + }); + }); + group('$NativeTexture', () { test('allocate', () async { final NativeTexture texture = await NativeTexture.allocate(); @@ -48,3 +84,31 @@ void main() { }); }); } + +class MockCameraDescription extends CameraDescription { + @override + LensDirection get direction => LensDirection.unknown; + + @override + String get name => 'none'; +} + +class MockCameraConfigurator extends CameraConfigurator { + @override + Future addPreviewTexture() => Future.value(7); + + @override + Future dispose() => Future.value(); + + @override + Future initialize() => Future.value(); + + @override + int get previewTextureId => 7; + + @override + Future start() => Future.value(); + + @override + Future stop() => Future.value(); +}