Closed15

flutter camera パッケージの Example を読んでいく

kenty (ケンティー)kenty (ケンティー)

途中で同じことをしている人を見つけました。そちらを見てください。
https://qiita.com/krohigewagma/items/66181e96a311390ebd5c


これ。
https://pub.dev/packages/camera/example

今後更新されるかもしれないので、現状のexampleソースコードをコピーしておく。
バージョンは0.10.5+9。早くバージョン1.0.0に到達して欲しい。
https://gist.github.com/ken-ty/26b583ba02a5008c79a3e5a9da6df0fb


※ Fluter 公式docs にもあるが、こちらは最低限って感じです。今回は見ません。
https://docs.flutter.dev/cookbook/plugins/picture-using-camera

kenty (ケンティー)kenty (ケンティー)

今回は android、iosをスコープに作業する。
はじめにandroid実機で動作確認し、その後iosでも動かす。

パッケージがないって言われるのでインストール。video_playerも入れるのね。

flutter pub add camera
flutter pub add video_player
kenty (ケンティー)kenty (ケンティー)

サンプルを android 実機(API33)で実行。早速エラー。
camera の README にあるように、android/app/build.gradleminSdkVersion を 21 以上にしないといけない。

表示されるエラー
Launching lib/main.dart on Pixel 7a in debug mode...
/Users/apple/ghq/github.com/ken-ty/flutter_camera_sample/android/app/src/debug/AndroidManifest.xml Error:
	uses-sdk:minSdkVersion 19 cannot be smaller than version 21 declared in library [:camera_android] /Users/apple/ghq/github.com/ken-ty/flutter_camera_sample/build/camera_android/intermediates/merged_manifest/debug/AndroidManifest.xml as the library might be using APIs not available in 19
	Suggestion: use a compatible library with a minSdk of at most 19,
		or increase this project's minSdk version to at least 21,
		or use tools:overrideLibrary="io.flutter.plugins.camera" to force usage (may lead to runtime failures)

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:processDebugMainManifest'.
> Manifest merger failed : uses-sdk:minSdkVersion 19 cannot be smaller than version 21 declared in library [:camera_android] /Users/apple/ghq/github.com/ken-ty/flutter_camera_sample/build/camera_android/intermediates/merged_manifest/debug/AndroidManifest.xml as the library might be using APIs not available in 19
  	Suggestion: use a compatible library with a minSdk of at most 19,
  		or increase this project's minSdk version to at least 21,
  		or use tools:overrideLibrary="io.flutter.plugins.camera" to force usage (may lead to runtime failures)

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 12s
kenty (ケンティー)kenty (ケンティー)

1056行あるmain.dartを見ていく。
ざっくり構成として、トップ階層にいる奴らを見ていく。

要素名 種類 説明
main メソッド 処理の開始地点。_camerasに有効なカメラを格納している。CameraAppを立ち上げている。
_cameras 変数 利用できるカメラ群。
CameraApp クラス よくある MyApp と同じ構成。homeに CameraExampleHome を指定している。
CameraExampleHome クラス カメラ画面。シンプルなStatefulWidget。
_CameraExampleHomeState クラス CameraExampleHome の Stete。主な実装はここ。
getCameraLensIcon メソッド 引数からカメラの向きに応じたアイコンを返す
_logError メソッド シンプルなログ出力メソッド

main で早々に _cameras = await availableCameras(); が実行されているのが気になった。
それ以外は、主な実装である _CameraExampleHomeState を読んでいけば良さそう。

kenty (ケンティー)kenty (ケンティー)

_CameraExampleHomeState には44メソッド定義されている。平均21行なので読みやすい。
メソッドを見る前に、 この class に with している Mixin を見ていく。

class _CameraExampleHomeState extends State<CameraExampleHome>
    with WidgetsBindingObserver, TickerProviderStateMixin {...}
Mixin 説明
WidgetsBindingObserver ライフサイクルを検知できたりする
TickerProviderStateMixin 複数アニメーションの為のcreateTickerメソッドを提供する

https://api.flutter.dev/flutter/widgets/WidgetsBindingObserver-class.html
https://api.flutter.dev/flutter/widgets/TickerProviderStateMixin-mixin.html

kenty (ケンティー)kenty (ケンティー)

WidgetsBindingObserver を mixin してライフサイクル変更時に処理を行う。
変更を検知する為に、initState で observer に自身を add している。dispose で remove することも忘れずに。

didChangeAppLifecycleState(state) を override して、 ライフサイクルが変更した時の動作を定義できる。

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    final CameraController? cameraController = controller;

    // App state changed before we got the chance to initialize.
    if (cameraController == null || !cameraController.value.isInitialized) {
      return;
    }

    // 非アクティブになったらコントローラを破棄
    if (state == AppLifecycleState.inactive) {
      cameraController.dispose();
    } else if (state == AppLifecycleState.resumed) {
      // 再開したらコントローラを初期化(disposeされたらdescriptionにアクセスできないのでは...?)
      _initializeCameraController(cameraController.description);
    }
  }
kenty (ケンティー)kenty (ケンティー)

_initializeCameraController(cameraDescription) を見てみる。
エラー列挙されていて、今後の実装の参考に助かる。
cameracontroller が ValueNotifier<CameraValue> を継承しており、ValueNotifier の仕様ちゃんと理解するべきだと感じた。
camera_controller.dart も読み込みたい。

  Future<void> _initializeCameraController(
      CameraDescription cameraDescription) async {
    final CameraController cameraController = CameraController(
      cameraDescription,
      kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium,
      enableAudio: enableAudio,
      imageFormatGroup: ImageFormatGroup.jpeg, // 指定できるんか、知らんかった。
    );

    controller = cameraController;

    // カメラコントローラが変更されたら発火する
    // If the controller is updated then update the UI.
    cameraController.addListener(() {
      // mounted == false の時に setStateを呼び出すとエラーになる為
     // https://api.flutter.dev/flutter/widgets/State/mounted.html
      if (mounted) {
        // UI 更新
        setState(() {});
      }
     // エラーならスナックバーを表示
      if (cameraController.value.hasError) { 
        showInSnackBar(
            'Camera error ${cameraController.value.errorDescription}');
      }
    });

    try {
      await cameraController.initialize();
      // 非同期実行。 [] の処理が全部終わるまで待機。
      await Future.wait(<Future<Object?>>[
        // The exposure mode is currently not supported on the web.
        ...!kIsWeb
            ? <Future<Object?>>[
                // 露出オフセットのmin-maxを更新
                cameraController.getMinExposureOffset().then(
                    (double value) => _minAvailableExposureOffset = value),
                cameraController
                    .getMaxExposureOffset()
                    .then((double value) => _maxAvailableExposureOffset = value)
              ]
            : <Future<Object?>>[],
        // zoomのmin-maxを更新
        cameraController
            .getMaxZoomLevel()
            .then((double value) => _maxAvailableZoom = value),
        // zoomのmin-maxを更新
            .getMinZoomLevel()
            .then((double value) => _minAvailableZoom = value),
      ]);
    } on CameraException catch (e) {
      switch (e.code) {
        case 'CameraAccessDenied':
          showInSnackBar('You have denied camera access.');
        case 'CameraAccessDeniedWithoutPrompt':
          // iOS only
          showInSnackBar('Please go to Settings app to enable camera access.');
        case 'CameraAccessRestricted':
          // iOS only
          showInSnackBar('Camera access is restricted.');
        case 'AudioAccessDenied':
          showInSnackBar('You have denied audio access.');
        case 'AudioAccessDeniedWithoutPrompt':
          // iOS only
          showInSnackBar('Please go to Settings app to enable audio access.');
        case 'AudioAccessRestricted':
          // iOS only
          showInSnackBar('Audio access is restricted.');
        default:
          _showCameraException(e);
          break;
      }
    }

    if (mounted) {
      setState(() {}); // 初期化が終わったらUI更新する
    }
  }
kenty (ケンティー)kenty (ケンティー)

_cameraPreviewWidget() をみると、CameraPreview(..., child: ... ) と、 プレビューの child に Widget を追加している。そんなことできたのか...!

kenty (ケンティー)kenty (ケンティー)
 @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    ...
    // App state changed before we got the chance to initialize.
    if (cameraController == null || !cameraController.value.isInitialized) {
      return;
    }

    // 非アクティブになったらコントローラを破棄
    if (state == AppLifecycleState.inactive) {
      cameraController.dispose();
    } else if (state == AppLifecycleState.resumed) {
      // 再開したらコントローラを初期化(disposeされたらdescriptionにアクセスできないのでは...?)
      _initializeCameraController(cameraController.description);
    }
  }

再開したらコントローラを初期化(disposeされたらdescriptionにアクセスできないのでは...?)について、disepose をちゃんと理解する。

dispose() 実行前後で cameraController を print してみると、同じ結果が得られた。

CameraController.dispose() の実装を見てみると、カメラのリソースの開放をしているだけで、controllerのインスタンス自体が破棄されるわけではなかった。つまり、セットしている discription にはアクセスできるということになる。

  /// Releases the resources of this camera.
  @override
  Future<void> dispose() async {
    if (_isDisposed) {
      return;
    }
    _unawaited(_deviceOrientationSubscription?.cancel());
    _isDisposed = true;
    super.dispose();
    if (_initializeFuture != null) {
      await _initializeFuture;
      await CameraPlatform.instance.dispose(_cameraId);
    }
  }
kenty (ケンティー)kenty (ケンティー)

iOS もビルドしておく。
差分として Podfile.lock, project.pbxproj, contents.xcworkspacedate が自動で発生した。コミットする。

カメラを起動しようとすると、案の定クラッシュ。知ってた。

This app has crashed because it attempted to access privacy-sensitive data without a usage description.  The app's Info.plist must contain an NSCameraUsageDescription key with a string value explaining to the user how the app uses this data.

camera の README にあるように、ios/Runner/Info.plist に以下を追加しないといけない。

<key>NSCameraUsageDescription</key>
<string>your usage description here</string>
<key>NSMicrophoneUsageDescription</key>
<string>your usage description here</string>

ビルドして、問題なくexampleが実行できることを確認。

このスクラップは2024/01/29にクローズされました