Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Camera fixes #2409

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
36 changes: 29 additions & 7 deletions packages/smooth_app/lib/helpers/camera_helper.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:smooth_app/pages/scan/camera_controller.dart';

class CameraHelper {
const CameraHelper._();

static List<CameraDescription>? _cameras;
static final CameraControllerNotifier _cameraControllerWrapper =
CameraControllerNotifier();

/// Ensure we have a single instance of this controller
/// /!\ Lazy-loaded
static SmoothCameraController? _controller;
static List<CameraDescription>? _cameras;

/// Mandatory method to call before [findBestCamera]
static Future<void> init() async {
Expand Down Expand Up @@ -66,12 +66,34 @@ class CameraHelper {
/// Init the controller
/// And prevents the redefinition of it
static void initController(SmoothCameraController controller) {
_controller ??= controller;
_cameraControllerWrapper._updateController(controller);
}

static void destroyControllerInstance() {
_controller = null;
_cameraControllerWrapper._updateController(null);
}

static SmoothCameraController? get controller => _controller;
static SmoothCameraController? get controller =>
_cameraControllerWrapper._controller;

/// Subscribe to this notifier to know when the controller is created /
/// destroyed
static CameraControllerNotifier get cameraControllerNotifier =>
_cameraControllerWrapper;
}

/// Custom implementation to prevent the use of a [ValueNotifier] which may be
/// limited in some case (@see [_updateController] below)
class CameraControllerNotifier extends ChangeNotifier {
SmoothCameraController? _controller;

void _updateController(SmoothCameraController? controller) {
try {
_controller = controller;
// Always call [notifyListeners] even if the value is similar
notifyListeners();
} catch (err) {
// If no Widget listens to us, an error may be thrown here
}
}
}
4 changes: 3 additions & 1 deletion packages/smooth_app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,9 @@ class _SmoothAppState extends State<SmoothApp> {
provide<ContinuousScanModel>(_continuousScanModel),
provide<SmoothAppDataImporter>(_appDataImporter),
provide<UpToDateProductProvider>(_upToDateProductProvider),
provide<PermissionListener>(_permissionListener)
provide<PermissionListener>(_permissionListener),
provide<CameraControllerNotifier>(
CameraHelper.cameraControllerNotifier),
],
builder: _buildApp,
);
Expand Down
77 changes: 77 additions & 0 deletions packages/smooth_app/lib/pages/scan/camera_image_preview.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/helpers/camera_helper.dart';
import 'package:smooth_app/pages/scan/camera_controller.dart';

/// Forked Widget from the [camera] library, but with a simpler content
class SmoothCameraStreamPreview extends StatelessWidget {
const SmoothCameraStreamPreview({
super.key,
});

@override
Widget build(BuildContext context) {
// Ensure this Widget is rebuild when necessary
Provider.of<CameraControllerNotifier>(
context,
listen: true,
);

final SmoothCameraController? cameraController = controller;
if (cameraController == null || cameraController.isInitialized != true) {
return const SizedBox.shrink();
}

return AspectRatio(
aspectRatio: _isLandscape()
? controller!.value.aspectRatio
: (1.0 / controller!.value.aspectRatio),
child: _wrapInRotatedBox(
child: controller!.buildPreview(),
),
);
}

Widget _wrapInRotatedBox({
required Widget child,
}) {
if (Platform.isIOS) {
return child;
}

return RotatedBox(
quarterTurns: _getQuarterTurns(),
child: child,
);
}

bool _isLandscape() {
return <DeviceOrientation>[
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight
].contains(_getApplicableOrientation());
}

int _getQuarterTurns() {
final Map<DeviceOrientation, int> turns = <DeviceOrientation, int>{
DeviceOrientation.portraitUp: 0,
DeviceOrientation.landscapeRight: 1,
DeviceOrientation.portraitDown: 2,
DeviceOrientation.landscapeLeft: 3,
};
return turns[_getApplicableOrientation()]!;
}

DeviceOrientation _getApplicableOrientation() {
return controller!.value.isRecordingVideo
? controller!.value.recordingOrientation!
: (controller!.value.previewPauseOrientation ??
controller!.value.lockedCaptureOrientation ??
controller!.value.deviceOrientation);
}

SmoothCameraController? get controller => CameraHelper.controller;
}
51 changes: 34 additions & 17 deletions packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'package:smooth_app/helpers/collections_helper.dart';
import 'package:smooth_app/pages/page_manager.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/camera_image_preview.dart';
import 'package:smooth_app/pages/scan/lifecycle_manager.dart';
import 'package:smooth_app/pages/scan/mkit_scan_helper.dart';
import 'package:smooth_app/pages/scan/scan_visor.dart';
Expand Down Expand Up @@ -108,12 +109,14 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>
}

// Relaunch the feed after a hot reload
if (_controller == null) {
_startLiveFeed();
} else {
_controller!.updateFocusPointAlgorithm(
_userPreferences.cameraFocusPointAlgorithm,
);
if (_isScreenVisible()) {
if (_controller == null) {
_startLiveFeed();
} else {
_controller!.updateFocusPointAlgorithm(
_userPreferences.cameraFocusPointAlgorithm,
);
}
}
}

Expand All @@ -127,9 +130,7 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>
Widget build(BuildContext context) {
return Consumer<BottomNavigationTab>(
builder: (BuildContext context, BottomNavigationTab tab, Widget? child) {
if (pendingResume &&
tab == BottomNavigationTab.Scan &&
Navigator.of(context).canPop()) {
if (pendingResume && _isScreenVisible(tab: tab)) {
pendingResume = false;
_onResumeImageStream();
}
Expand All @@ -149,6 +150,14 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>
);
}

/// Returns if the current tab is visible AND the scanner is also visible
/// (= the first element = canPop == false)
bool _isScreenVisible({BottomNavigationTab? tab}) {
return (tab ?? Provider.of<BottomNavigationTab>(context, listen: false)) ==
BottomNavigationTab.Scan &&
!Navigator.of(context).canPop();
}

Widget _buildScannerWidget() {
// Showing a black scanner background when the camera is not initialized
if (!isCameraReady) {
Expand All @@ -167,9 +176,7 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>
scale: _previewScale,
child: Center(
key: ValueKey<bool>(stoppingCamera),
child: CameraPreview(
_controller!,
),
child: const SmoothCameraStreamPreview(),
),
);
},
Expand Down Expand Up @@ -298,8 +305,10 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>
});
}
} on CameraException catch (e) {
// TODO(M123): Show error message
Logs.d('On camera error', ex: e);

// The camera may not be ready. Wait a short time and try again.
return _stopImageStream();
} on FlutterError catch (e) {
Logs.d('On camera (Flutter part) error', ex: e);
}
Expand Down Expand Up @@ -389,7 +398,12 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>
}

if (_controller?.isPauseResumePreviewSupported == true) {
await _controller?.resumePreviewIfNecessary();
try {
await _controller?.resumePreviewIfNecessary();
} on CameraException catch (_) {
// Dart Controller is OK, but native part is KO
return _stopImageStream();
}
}
stoppingCamera = false;
}
Expand All @@ -408,11 +422,14 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>
_redrawScreen();

_controller?.removeListener(_cameraListener);
await _streamSubscription?.cancel();

await _controller?.dispose();
// Don't wait for the controller to be disposed,
// a new one will be created in parallel
_controller?.dispose();
CameraHelper.destroyControllerInstance();

await _streamSubscription?.cancel();

await _barcodeDecoder?.dispose();
_barcodeDecoder = null;

Expand Down Expand Up @@ -446,7 +463,7 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>
DateTime.now().difference(referentialTime).inMilliseconds;

// The screen is still visible, we should restart the camera
if (diff < _minPostFrameCallbackDelay) {
if (diff < _minPostFrameCallbackDelay && _isScreenVisible()) {
_startLiveFeed();
}
});
Expand Down
6 changes: 5 additions & 1 deletion packages/smooth_app/lib/widgets/lifecycle_aware_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ abstract class LifecycleAwareState<T extends StatefulWidget> extends State<T> {
/// Will call [setState] only if the current lifecycle state allows it
void setStateSafe(VoidCallback fn) {
if (_debugLifecycleState != StateLifecycle.defunct) {
setState(fn);
try {
setState(fn);
} catch (ignored) {
// ignore
}
}
}

Expand Down
20 changes: 10 additions & 10 deletions packages/smooth_app/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ packages:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "40.0.0"
version: "41.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
version: "4.2.0"
archive:
dependency: transitive
description:
Expand Down Expand Up @@ -125,7 +125,7 @@ packages:
description:
path: "packages/camera/camera"
ref: smooth_camera
resolved-ref: bea3b8e065b89ee80f85f2c20ada24e21facf531
resolved-ref: d85e1a0df41547cb48c912afc48915dc81a6370a
url: "https://github.com/g123k/plugins.git"
source: git
version: "0.9.6"
Expand All @@ -134,7 +134,7 @@ packages:
description:
path: "packages/camera/camera_platform_interface"
ref: smooth_camera
resolved-ref: bea3b8e065b89ee80f85f2c20ada24e21facf531
resolved-ref: d85e1a0df41547cb48c912afc48915dc81a6370a
url: "https://github.com/g123k/plugins.git"
source: git
version: "2.1.6"
Expand All @@ -143,7 +143,7 @@ packages:
description:
path: "packages/camera/camera_web"
ref: smooth_camera
resolved-ref: bea3b8e065b89ee80f85f2c20ada24e21facf531
resolved-ref: d85e1a0df41547cb48c912afc48915dc81a6370a
url: "https://github.com/g123k/plugins.git"
source: git
version: "0.2.1+6"
Expand Down Expand Up @@ -448,7 +448,7 @@ packages:
name: flutter_svg
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
version: "1.1.1+1"
flutter_test:
dependency: "direct dev"
description: flutter
Expand Down Expand Up @@ -519,7 +519,7 @@ packages:
name: hive
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.2"
version: "2.2.3"
hive_flutter:
dependency: "direct main"
description:
Expand Down Expand Up @@ -843,7 +843,7 @@ packages:
name: path_provider_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.15"
version: "2.0.16"
path_provider_ios:
dependency: transitive
description:
Expand Down Expand Up @@ -1011,14 +1011,14 @@ packages:
name: sentry
url: "https://pub.dartlang.org"
source: hosted
version: "6.6.0"
version: "6.6.1"
sentry_flutter:
dependency: "direct main"
description:
name: sentry_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "6.6.0"
version: "6.6.1"
share_plus:
dependency: "direct main"
description:
Expand Down