Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
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
85 changes: 73 additions & 12 deletions packages/camera/lib/new/src/camera_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<CameraDescription> cameras = async CameraController.availableCameras();
/// final CameraController controller = CameraController(description: cameras[0]);
/// controller.initialize();
/// controller.start();
/// ```
class CameraController {
/// Default constructor.
///
Expand All @@ -28,7 +35,6 @@ class CameraController {
///
/// This will choose the best [CameraConfigurator] for the current device.
factory CameraController({@required CameraDescription description}) {
assert(description != null);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unnecessary since its in the constructor.

return CameraController._(
description: description,
configurator: _createDefaultConfigurator(description),
Expand Down Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

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

May I offer a different view about this approach? How about not having it as a static field? If the client developer unwisely calls initialize without disposing the previous CameraController, this may be a error, but also that may be that he is trying to access more than one camera at the same time (I don't know if this is possible in the hardware layer), but seems feasible to have more than one camera open and this to be alright.

Even if there is no chance of the developer accessing more than one camera at the same time, I believe that an exception should be raised explaining that the device is busy.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the review!

Our reasoning to keep this functionality is because the current implementation of the plugin only allows one controller to be active at a time. It isn't documented in the plugin, but that is how it works underneath. While we refactor the platform specific code, we want to keep the interface the same and not cause many breaking changes.

The other reason for this functionality is because the CameraController will only serve as a simple to use and narrowly defined interface usable on any platform. For more complex features (like accessing multiple cameras), we will also expose access to this through another interface or require platform specific code.


bool _isDisposed = false;

/// Details for the camera this controller accesses.
final CameraDescription description;

Expand All @@ -68,21 +82,63 @@ 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.
static Future<List<CameraDescription>> availableCameras() async {
throw UnimplementedError('$defaultTargetPlatform not supported');
}

/// Begins the flow of data between the inputs and outputs connected the camera instance.
Future<void> 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<void> initialize() {
if (_instance == this) {
return Future<void>.value();
}

final Completer<void> completer = Completer<void>();

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<void> 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<void> stop() {
assert(!_isDisposed, _isDisposedMessage);
assert(_instance != this, _isNotInitializedMessage);

/// Stops the flow of data between the inputs and outputs connected the camera instance.
Future<void> stop() => configurator.stop();
return configurator.stop();
}

/// Deallocate all resources and disables further use of the controller.
Future<void> dispose() => configurator.dispose();
Future<void> dispose() {
_instance = null;
_isDisposed = true;
return configurator.dispose();
}

static CameraConfigurator _createDefaultConfigurator(
CameraDescription description,
Expand All @@ -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',
);
*/
}
}
7 changes: 5 additions & 2 deletions packages/camera/lib/new/src/common/camera_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> 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<void> 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<void> stop();

/// Dispose all resources and disables further use of this configurator.
Expand Down
64 changes: 64 additions & 0 deletions packages/camera/test/camera_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<AssertionError>()),
);

expect(
() => controller1.stop(),
throwsA(isInstanceOf<AssertionError>()),
);

expect(controller1.isDisposed, isTrue);
});
});

group('$NativeTexture', () {
test('allocate', () async {
final NativeTexture texture = await NativeTexture.allocate();
Expand All @@ -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<int> addPreviewTexture() => Future<int>.value(7);

@override
Future<void> dispose() => Future<void>.value();

@override
Future<void> initialize() => Future<void>.value();

@override
int get previewTextureId => 7;

@override
Future<void> start() => Future<void>.value();

@override
Future<void> stop() => Future<void>.value();
}