Android EmulatorでWebカメラの映像を出力する方法【Flutter】

2021-11-11

はじめに

ナンのネタも思いつかねぇなと日々を過ごし、怠惰に過ごし生きています。

ここ一年ぐらいずっと炎上しているプロジェクトにぶち込まれて、朝の7時~夜10時みたいな日もちらほら出てきました。 前職よりも朝が早いので在宅じゃなければ身体が持たなかったことでしょう。助かりました。

最近、徐々に頼まれごとでスクリプトやツールのコードを書くことが増えてきました。 やる気がお散歩している日に、僕の代わりに仕事やってくれるようにと コソコソとツール作って動かしていたのがバレてきたのでしょう。

頼られることは実に良いことなんですが業務が増えるばかりですので持っていって欲しいなと心の片隅に思うこともありますが…恩は売れるときに売れるだけ売っておくのが良いと古事記にも書かれています。セール期間と思って耐えましょう。

ただ非常に興味深いのは、聞いてみるとスクリプト書けば楽できるのにって案件が結構あるところが気づきです。

この売った恩という種が花開き、いつか形を変えて私を助けてくれると信じてます。

偉そうなこと行ってますが大体助けてもらっているので、恩を少しは返せてよかったという気持ちもあります。 恩を仇で返すような大人にならずに済んだと喜ぶべきところです。ここは。

最近作った中でシンプルだけどいい出来なのが、EXCELで自動更新型のガントチャート作成したやつです。作ったのはいいけど最近はスクラム系のツールでそんな機能が一通り揃ってんですよね。わざわざ作らずとも…という話は確かにありました。

チーム内でもこの前新しくRedmineは立ち上げて、チケットの自動登録系スクリプトも書いてあるからそっちで頑張る方向もありましたが、対話してみるとEXCELが良いって結論づいたので作ったんですよ。

新しいものは抵抗があるのも分かるし、Redmineでやろうとするとツールに振り回されそうな気もしていたのでEXCELでまずは良いと思うのですがちょっと複雑な気持ちですよね。

実際はそんなリッチなものはいらなかったかもねなりそうだったし、 もしそうなると再導入がしづらくなるので、手頃に始めることができるところからやる戦略も一つだと思います。今回はそんなスモール戦略の一つかと思います。

神EXCEL(笑)

世間のエンジニア様は、神EXCELwwwって揶揄してますが、中々馬鹿にできないと思います。 ボタンとかのGUIがローコストで作れるし、プログラムせずともGUIが付いてるのは大きいし 色んな人が抵抗なく使ってくれるってのはアドバンテージだと思うんですよね。

そういう意味では、私結構好きなんですよEXCEL。 別のツールからも読み出しやすく作ってくれてればなお良いですね。EXCEL君。

そら中には居ますよ? 画像を貼り付けまくって表計算ソフトとしての体裁を忘れたやついますよ?あいつ辛いですが… Markdownにして欲しいとか思いますし、せめてWordやOnenoteに移行してほしいとは思いますが…。

さて前置きが日記状態ですが、今日はそんな話とは一切関係無いことを書いていきます。 Android EmulatorでWebCameraを使う方法です。

子供用にアプリでも作って遊んでもらおうと思い立って調べていました。

Android EmulatorでWebCameraを使う

Flutterからカメラを試そうとしたときに、Emulatorから動かすことができます。

ただ、QRコード読み取りとか作ろうとするとどうしても現実の映像を表示したくなるのですが、 デフォルトだと部屋の中を歩くエミュレータが表示されるんですよ。

Altボタン押しながら、WASDで前後左右に移動できます。 QとEで高さを調整できます。 かなりリッチだなと驚いてます。

カメラは前と後ろあって、2種類ぐらいEmulateできます。 Front Cameraだとサボテンダーみたいなのが表示されます。

Android Studioを開いてAVD ManagerからEmulatorの設定を行う。

CameraからEmulatedとなっているところを、Webcam0などに設定する。

これでOKです。

スキャナコード

趣旨から外れるので、テストコードは非常にExampleをそのまま使います。

barcode_scan2: example

flutter pub add barcode_scan2

lib/main.dart

コードはこのように記載しています。 Examplesをそのまま引用しており何の捻りもないです。 また別途、内容を読み解いて記事に落とせればと思います。

import 'dart:async';
import 'dart:io' show Platform;

import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(const App());

class App extends StatefulWidget {
  const App({Key? key}) : super(key: key);
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  ScanResult? scanResult;

  final _flashOnController = TextEditingController(text: 'Flash on');
  final _flashOffController = TextEditingController(text: 'Flash off');
  final _cancelController = TextEditingController(text: 'Cancel');

  var _aspectTolerance = 0.00;
  var _numberOfCameras = 0;
  var _selectedCamera = -1;
  var _useAutoFocus = true;
  var _autoEnableFlash = false;

  static final _possibleFormats = BarcodeFormat.values.toList()
    ..removeWhere((e) => e == BarcodeFormat.unknown);

  List<BarcodeFormat> selectedFormats = [..._possibleFormats];

  @override
  void initState() {
    super.initState();

    Future.delayed(Duration.zero, () async {
      _numberOfCameras = await BarcodeScanner.numberOfCameras;
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    final scanResult = this.scanResult;
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Barcode Scanner Example'),
          actions: [
            IconButton(
              icon: const Icon(Icons.camera),
              tooltip: 'Scan',
              onPressed: _scan,
            )
          ],
        ),
        body: ListView(
          scrollDirection: Axis.vertical,
          shrinkWrap: true,
          children: <Widget>[
            if (scanResult != null)
              Card(
                child: Column(
                  children: <Widget>[
                    ListTile(
                      title: const Text('Result Type'),
                      subtitle: Text(scanResult.type.toString()),
                    ),
                    ListTile(
                      title: const Text('Raw Content'),
                      subtitle: Text(scanResult.rawContent),
                    ),
                    ListTile(
                      title: const Text('Format'),
                      subtitle: Text(scanResult.format.toString()),
                    ),
                    ListTile(
                      title: const Text('Format note'),
                      subtitle: Text(scanResult.formatNote),
                    ),
                  ],
                ),
              ),
            const ListTile(
              title: Text('Camera selection'),
              dense: true,
              enabled: false,
            ),
            RadioListTile(
              onChanged: (v) => setState(() => _selectedCamera = -1),
              value: -1,
              title: const Text('Default camera'),
              groupValue: _selectedCamera,
            ),
            ...List.generate(
              _numberOfCameras,
              (i) => RadioListTile(
                onChanged: (v) => setState(() => _selectedCamera = i),
                value: i,
                title: Text('Camera ${i + 1}'),
                groupValue: _selectedCamera,
              ),
            ),
            const ListTile(
              title: Text('Button Texts'),
              dense: true,
              enabled: false,
            ),
            ListTile(
              title: TextField(
                decoration: const InputDecoration(
                  floatingLabelBehavior: FloatingLabelBehavior.always,
                  labelText: 'Flash On',
                ),
                controller: _flashOnController,
              ),
            ),
            ListTile(
              title: TextField(
                decoration: const InputDecoration(
                  floatingLabelBehavior: FloatingLabelBehavior.always,
                  labelText: 'Flash Off',
                ),
                controller: _flashOffController,
              ),
            ),
            ListTile(
              title: TextField(
                decoration: const InputDecoration(
                  floatingLabelBehavior: FloatingLabelBehavior.always,
                  labelText: 'Cancel',
                ),
                controller: _cancelController,
              ),
            ),
            if (Platform.isAndroid) ...[
              const ListTile(
                title: Text('Android specific options'),
                dense: true,
                enabled: false,
              ),
              ListTile(
                title: Text(
                  'Aspect tolerance (${_aspectTolerance.toStringAsFixed(2)})',
                ),
                subtitle: Slider(
                  min: -1,
                  max: 1,
                  value: _aspectTolerance,
                  onChanged: (value) {
                    setState(() {
                      _aspectTolerance = value;
                    });
                  },
                ),
              ),
              CheckboxListTile(
                title: const Text('Use autofocus'),
                value: _useAutoFocus,
                onChanged: (checked) {
                  setState(() {
                    _useAutoFocus = checked!;
                  });
                },
              ),
            ],
            const ListTile(
              title: Text('Other options'),
              dense: true,
              enabled: false,
            ),
            CheckboxListTile(
              title: const Text('Start with flash'),
              value: _autoEnableFlash,
              onChanged: (checked) {
                setState(() {
                  _autoEnableFlash = checked!;
                });
              },
            ),
            const ListTile(
              title: Text('Barcode formats'),
              dense: true,
              enabled: false,
            ),
            ListTile(
              trailing: Checkbox(
                tristate: true,
                materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
                value: selectedFormats.length == _possibleFormats.length
                    ? true
                    : selectedFormats.isEmpty
                        ? false
                        : null,
                onChanged: (checked) {
                  setState(() {
                    selectedFormats = [
                      if (checked ?? false) ..._possibleFormats,
                    ];
                  });
                },
              ),
              dense: true,
              enabled: false,
              title: const Text('Detect barcode formats'),
              subtitle: const Text(
                'If all are unselected, all possible '
                'platform formats will be used',
              ),
            ),
            ..._possibleFormats.map(
              (format) => CheckboxListTile(
                value: selectedFormats.contains(format),
                onChanged: (i) {
                  setState(() => selectedFormats.contains(format)
                      ? selectedFormats.remove(format)
                      : selectedFormats.add(format));
                },
                title: Text(format.toString()),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Future<void> _scan() async {
    try {
      final result = await BarcodeScanner.scan(
        options: ScanOptions(
          strings: {
            'cancel': _cancelController.text,
            'flash_on': _flashOnController.text,
            'flash_off': _flashOffController.text,
          },
          restrictFormat: selectedFormats,
          useCamera: _selectedCamera,
          autoEnableFlash: _autoEnableFlash,
          android: AndroidOptions(
            aspectTolerance: _aspectTolerance,
            useAutoFocus: _useAutoFocus,
          ),
        ),
      );
      setState(() => scanResult = result);
    } on PlatformException catch (e) {
      setState(() {
        scanResult = ScanResult(
          type: ResultType.Error,
          format: BarcodeFormat.unknown,
          rawContent: e.code == BarcodeScanner.cameraAccessDenied
              ? 'The user did not grant the camera permission!'
              : 'Unknown error: $e',
        );
      });
    }
  }
}

まとめ

最近子供がままごとを始めて、野菜とかを駄菓子の入れ物で触りながら「ピッ」って言うてます。 アンパンマンのレジスターとか買ってあげても良いのですが、あまりに味気ないなと思い レジ打ち遊びができるようにしてあげたいなと思っています。

ArudioやRaspi zeroがあるのでそのへんで作ってもいいですが、 電池やバッテリをおもちゃに組み込んで渡すのもまだ抵抗があります。

スマホなら許容できるかしらということでアプリを作ろうと思いたちます。

Flutterでバーコードスキャナー兼、なんかそれっぽい音と画面表示が出るアプリでも習作がてら作るの良いかもと考え、 EmulatorでWEBカメラって使えるのかと調べたのがこの記事の発端です。

業務でRustを使ってみようという案件がありそうな話が聞こえてきたのでしばらくはそこを押さえる記事を書きます。

Flutterアプリの野望はそんなところです。