feat: Camera controller as a singleton (#1873)

* Improvements about resuming the camera

* Remove unused print call

* Singleton for the CameraController
This commit is contained in:
Edouard Marquez
2022-05-17 19:55:23 +02:00
committed by GitHub
parent 12b493379a
commit 195f9db6a4
5 changed files with 199 additions and 66 deletions

View File

@ -1,10 +1,15 @@
import 'package:camera/camera.dart';
import 'package:smooth_app/pages/scan/camera_controller.dart';
class CameraHelper {
const CameraHelper._();
static List<CameraDescription>? _cameras;
/// Ensure we have a single instance of this controller
/// /!\ Lazy-loaded
static SmoothCameraController? _controller;
/// Mandatory method to call before [findBestCamera]
static Future<void> init() async {
_cameras = await availableCameras();
@ -48,4 +53,12 @@ class CameraHelper {
return _cameras![cameraIndex];
}
/// Init the controller
/// And prevents the redefinition of it
static void initController(SmoothCameraController controller) {
_controller ??= controller;
}
static SmoothCameraController? get controller => _controller;
}

View File

@ -0,0 +1,96 @@
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// A lifecycle-aware [CameraController]
class SmoothCameraController extends CameraController {
SmoothCameraController(
CameraDescription description,
ResolutionPreset resolutionPreset, {
bool? enableAudio,
ImageFormatGroup? imageFormatGroup,
}) : _isPaused = false,
_isInitialized = false,
super(
description,
resolutionPreset,
enableAudio: enableAudio ?? true,
imageFormatGroup: imageFormatGroup,
);
/// Status of the preview
bool _isPaused;
/// Status of the controller
bool _isInitialized;
Future<void> init({
required FocusMode focusMode,
required Offset focusPoint,
required DeviceOrientation deviceOrientation,
required onLatestImageAvailable onAvailable,
}) async {
if (!_isInitialized) {
await initialize();
await setFocusMode(focusMode);
await setFocusPoint(focusPoint);
await setExposurePoint(focusPoint);
await lockCaptureOrientation(deviceOrientation);
await startImageStream(onAvailable);
_isInitialized = true;
}
}
/// Please use [init] instead
@protected
@override
Future<void> initialize() => super.initialize();
@override
Future<void> startImageStream(onLatestImageAvailable onAvailable) {
final Future<void> startImageStreamResult =
super.startImageStream(onAvailable);
_isPaused = false;
return startImageStreamResult;
}
@override
Future<void> pausePreview() {
final Future<void> pausePreviewResult = super.pausePreview();
_isPaused = true;
return pausePreviewResult;
}
Future<void> resumePreviewIfNecessary() async {
if (_isPaused) {
return resumePreview();
}
}
/// Please use [resumePreviewIfNecessary] instead
@protected
@override
Future<void> resumePreview() {
final Future<void> resumePreviewResult = super.resumePreview();
_isPaused = false;
return resumePreviewResult;
}
@override
Future<void> stopImageStream() {
final Future<void> stopImageStreamResult = super.stopImageStream();
_isPaused = false;
return stopImageStreamResult;
}
@override
Future<void> dispose() {
final Future<void> disposeResult = super.dispose();
_isPaused = false;
_isInitialized = false;
return disposeResult;
}
bool get isPaused => _isPaused;
bool get isInitialized => _isInitialized;
}

View File

@ -7,21 +7,26 @@ import 'package:visibility_detector/visibility_detector.dart';
/// [onStart] will be called only when the Widget is displayed for the first time
/// (= during the [initState] phase)
/// [onResume] will be called once the app is reopened (eg: the app is minimized
/// and brought back to front) or this part of the Widget tree is visible again
/// [onPause] will be called once the app is minimized or if this part of the
/// tree is invisible
/// and brought back to front)
/// [onPause] will be called once the app is minimized
/// [onVisible] will be called if this part of the tree is visible
/// [onInvisible] will be called if this part of the tree is invisible
class LifeCycleManager extends StatefulWidget {
const LifeCycleManager({
required this.onResume,
required this.onPause,
required this.child,
this.onStart,
this.onVisible,
this.onInvisible,
Key? key,
}) : super(key: key);
final Function() onResume;
final Function() onPause;
final Function()? onStart;
final Function()? onVisible;
final Function()? onInvisible;
final Widget child;
@override
@ -68,9 +73,9 @@ class LifeCycleManagerState extends State<LifeCycleManager>
void _onVisibilityChanged(bool visible) {
if (visible) {
widget.onResume();
widget.onVisible?.call();
} else {
widget.onPause();
widget.onInvisible?.call();
}
}

View File

@ -11,6 +11,7 @@ import 'package:smooth_app/data_models/user_preferences.dart';
import 'package:smooth_app/helpers/camera_helper.dart';
import 'package:smooth_app/helpers/collections_helper.dart';
import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart';
import 'package:smooth_app/pages/scan/camera_controller.dart';
import 'package:smooth_app/pages/scan/lifecycle_manager.dart';
import 'package:smooth_app/pages/scan/mkit_scan_helper.dart';
import 'package:smooth_app/widgets/lifecycle_aware_widget.dart';
@ -70,7 +71,6 @@ class MLKitScannerPageState
late ContinuousScanModel _model;
late UserPreferences _userPreferences;
CameraController? _controller;
CameraDescription? _camera;
double _previewScale = 1.0;
@ -106,8 +106,10 @@ class MLKitScannerPageState
// all entry points
return LifeCycleManager(
onStart: _startLiveFeed,
onResume: _startLiveFeed,
onPause: () => _stopImageStream(fromPauseEvent: true),
onResume: _onResumeImageStream,
onVisible: () => _onResumeImageStream(forceStartPreview: true),
onPause: _onPauseImageStream,
onInvisible: _onPauseImageStream,
child: _buildScannerWidget(),
);
}
@ -146,28 +148,17 @@ class MLKitScannerPageState
);
}
bool get isCameraNotInitialized {
return _controller == null ||
_controller!.value.isInitialized == false ||
stoppingCamera ||
_controller!.value.isPreviewPaused ||
!_controller!.value.isStreamingImages;
}
bool get isCameraNotInitialized => _controller?.isInitialized != true;
Future<void> _startLiveFeed() async {
if (_controller != null || _camera == null) {
if (_camera == null) {
return;
} else if (_controller != null) {
return _onResumeImageStream();
}
stoppingCamera = false;
_controller = CameraController(
_camera!,
ResolutionPreset.medium,
enableAudio: false,
imageFormatGroup: ImageFormatGroup.yuv420,
);
// If the controller is initialized update the UI.
_barcodeDecoder ??= MLKitScanDecoder(
camera: _camera!,
@ -177,47 +168,49 @@ class MLKitScannerPageState
),
),
);
_controller?.addListener(_cameraListener);
// Restart the subscription if necessary
if (_streamSubscription?.isPaused == true) {
_streamSubscription!.resume();
} else {
_subject
.throttleTime(
Duration(
milliseconds:
_averageProcessingTime.average(_defaultProcessingTime) *
_processingTimeWindows,
),
)
.asyncMap((CameraImage image) async {
final DateTime start = DateTime.now();
CameraHelper.initController(
SmoothCameraController(
_camera!,
ResolutionPreset.medium,
enableAudio: false,
imageFormatGroup: ImageFormatGroup.yuv420,
),
);
final List<String?>? res =
await _barcodeDecoder?.processImage(image);
_controller!.addListener(_cameraListener);
_averageProcessingTime.add(
DateTime.now().difference(start).inMilliseconds,
);
_subject
.throttleTime(
Duration(
milliseconds:
_averageProcessingTime.average(_defaultProcessingTime) *
_processingTimeWindows,
),
)
.asyncMap((CameraImage image) async {
final DateTime start = DateTime.now();
return res;
})
.where(
(List<String?>? barcodes) => barcodes?.isNotEmpty == true,
)
.cast<List<String>>()
.listen(_onNewBarcodeDetected);
}
final List<String?>? res = await _barcodeDecoder?.processImage(image);
_averageProcessingTime.add(
DateTime.now().difference(start).inMilliseconds,
);
return res;
})
.where(
(List<String?>? barcodes) => barcodes?.isNotEmpty == true,
)
.cast<List<String>>()
.listen(_onNewBarcodeDetected);
try {
await _controller?.initialize();
await _controller?.setFocusMode(FocusMode.auto);
await _controller?.setFocusPoint(_focusPoint);
await _controller?.setExposurePoint(_focusPoint);
await _controller?.lockCaptureOrientation(DeviceOrientation.portraitUp);
await _controller?.startImageStream(
(CameraImage image) => _subject.add(image),
await _controller?.init(
focusMode: FocusMode.auto,
focusPoint: _focusPoint,
deviceOrientation: DeviceOrientation.portraitUp,
onAvailable: (CameraImage image) => _subject.add(image),
);
} on CameraException catch (e) {
if (kDebugMode) {
@ -238,33 +231,55 @@ class MLKitScannerPageState
}
void _cameraListener() {
_redrawScreen();
if (_controller?.value.hasError == true) {
// TODO(M123): Handle errors better
debugPrint(_controller!.value.errorDescription);
}
}
Future<void> _stopImageStream({bool fromPauseEvent = false}) async {
Future<void> _onPauseImageStream() async {
if (stoppingCamera) {
return;
}
_controller?.pausePreview();
_streamSubscription?.pause();
}
Future<void> _onResumeImageStream({bool forceStartPreview = false}) async {
if (stoppingCamera ||
(!forceStartPreview && ScreenVisibilityDetector.invisible(context))) {
return;
}
_controller?.resumePreviewIfNecessary();
stoppingCamera = false;
if (_streamSubscription?.isPaused == true) {
_streamSubscription!.resume();
}
}
Future<void> _stopImageStream() async {
if (stoppingCamera) {
return;
}
_controller?.pausePreview();
stoppingCamera = true;
_redrawScreen();
_controller?.removeListener(_cameraListener);
if (fromPauseEvent) {
_streamSubscription?.pause();
} else {
await _streamSubscription?.cancel();
}
await _streamSubscription?.cancel();
await _controller?.dispose();
await _barcodeDecoder?.dispose();
_barcodeDecoder = null;
_controller = null;
_restartCameraIfNecessary();
}
@ -306,7 +321,7 @@ class MLKitScannerPageState
void dispose() {
// /!\ This call is a Future, which may leads to some issues.
// This should be handled by [_restartCameraIfNecessary]
_stopImageStream(fromPauseEvent: false);
_stopImageStream();
super.dispose();
}
@ -321,4 +336,6 @@ class MLKitScannerPageState
return Offset(0.5, 0.25 / _previewScale);
}
}
SmoothCameraController? get _controller => CameraHelper.controller;
}

View File

@ -16,6 +16,8 @@ class ScreenVisibilityDetector extends StatefulWidget {
static bool visible(BuildContext context) =>
context.read<ScreenVisibility>().isVisible;
static bool invisible(BuildContext context) => !visible(context);
}
class _ScreenVisibilityDetectorState extends State<ScreenVisibilityDetector> {