狛ログ

2021年12月27日月曜日

Angular・ngx-scannerでインカメラが使われるのを制御する方法。


こんにちは、オフィス狛 技術部のpinoです。


Angularで、バーコードやQRコード読み取りに使用するライブラリに、『ngx-scanner』があります。

テンプレートに<zxing-scanner></zxing-scanner>を埋め込むだけで、読み取り用のカメラを自動で起動してくれる便利なライブラリです。


当社のプロジェクトでも採用しているのですが、先日、通常なら背面のカメラが起動するところで、iOS12でのみインカメラが起動する事象が発生しました。


今回は、その時のトラブルシューティングとして、インカメラが使われるのを制御する方法について紹介します。



バージョン情報

  • Angular:12.0.3
  • ngx-scanner:3.0.1


なぜインカメラが起動する?

まず、インカメラが起動してしまう理由について見ていきます。


カメラが起動するまでの流れは、node_modules/@zxing/ngx-scanner/__ivy_ngcc__/fesm2015/zxing-ngx-scanner.jsで確認できます。

ngAfterViewInit()

/**
 * Executed after the view initialization.
 */
ngAfterViewInit() {
    // makes torch availability information available to user
    this.getCodeReader().isTorchAvailable.subscribe(x => this.torchCompatible.emit(x));
    if (!this.autostart) {
        console.warn('New feature \'autostart\' disabled, be careful. Permissions and devices recovery has to be run manually.');
        // does the necessary configuration without autostarting
        this.initAutostartOff();
        return;
    }
    // configurates the component and starts the scanner
    this.initAutostartOn();
}

this.autostartはtrue固定なのでif (!this.autostart)の分岐に入りません。
this.initAutostartOn()に続きます。

initAutostartOn()

/**
 * Initializes the component and starts the scanner.
 * Permissions are asked to accomplish that.
 */
initAutostartOn() {
    return __awaiter(this, void 0, void 0, function* () {
        this.isAutostarting = true;
        let hasPermission;
        try {
            // Asks for permission before enumerating devices so it can get all the device's info
            hasPermission = yield this.askForPermission();
        }
        catch (e) {
            console.error('Exception occurred while asking for permission:', e);
            return;
        }
        // from this point, things gonna need permissions
        if (hasPermission) {
            const devices = yield this.updateVideoInputDevices();
            this.autostartScanner([...devices]);
        }
    });
}

カメラアクセスの権限周りをチェックしています。

許可された状態だと、最終的にthis.autostartScanner([...devices])が実行されます。

autostartScanner()

/**
 * Starts the scanner with the back camera otherwise take the last
 * available device.
 */
autostartScanner(devices) {
    const matcher = ({ label }) => /back|trás|rear|traseira|environment|ambiente/gi.test(label);
    // select the rear camera by default, otherwise take the last camera.
    const device = devices.find(matcher) || devices.pop();
    if (!device) {
        throw new Error('Impossible to autostart, no input devices available.');
    }
    this.device = device;
    // @note when listening to this change, callback code will sometimes run before the previous line.
    this.deviceChange.emit(device);
    this.isAutostarting = false;
    this.autostarted.next();
}

まず、カメラのlabelプロパティにback|trás|rear|traseira|environment|ambienteが含まれるかを見ていますね。

(調べてみたところ、ポルトガル語やスペイン語などで「後方」「環境」といった意味の単語が含まれているようです。)

devicesの中で上記に当てはまる最初のカメラか、当てはまるものがない場合はdevicesの最後尾のカメラが起動される仕組みになっています。


ここまでで、カメラが起動するまでの流れを追うことができました。


次は、iOS12と、比較用に背面カメラが起動するiOS15のカメラ情報を、デモページを使って確認してみます。


デモページでカメラアクセスを許可すると、使用できるカメラのリストが表示されます。

iOS12とiOS15で試した結果、以下のようになりました。

『〇〇カメラ』となっているのがlabelプロパティに当たり、並び順がそのままdevicesの要素の並びになっています。


どちらのOSも、labelback|trás|rear|traseira|environment|ambienteは含まれていません。

その場合、配列の最後尾のカメラが起動されるわけですが、iOS15は背面カメラなのに対し、iOS12は前面側カメラが最後尾になっています。

iOS12でのみインカメラが起動するのは、ここに原因があったんですね。

デモのコードは、公式のGithubか下記からも確認できます。

https://stackblitz.com/edit/zxing-ngx-scanner


ちなみに、iPhoneの設定から『iPhoneの使用言語』をEnglishに変更したところ、

  • 前面カメラ -> Front Camera

  • 背面カメラ -> Back Camera

という風に文言が変わりました。

labelは使用言語に基づくようなので、使用言語を変更することで回避できる、ということになりますが、あまり現実的ではなさそうです...。



インカメラが使われるのを避けるには?

自動起動されるカメラを変更するのは、先ほど確認したような処理の流れ自体を変更しないといけないので、難しいです。

ただ、デモページで確認できるように、カメラの切り替えは可能になっています。

だとしたら、自動起動が終わった後に、背面カメラを起動するよう設定し直すのはどうでしょうか?


ということで、今度は「自動起動が完了したことを検知できるか」を確認してみます。


ngx-scannerでは、いくつかのイベントが用意されています。

今回使用しているVer.3.0.1では、以下のイベントが定義されていました。

  • autostarted

  • autostarting

  • torchCompatible

  • scanSuccess

  • scanFailure

  • scanError

  • scanComplete

  • camerasFound

  • camerasNotFound

  • permissionResponse

  • hasDevices

  • deviceChange

名前からなんとなくどんなイベントか予想がつくかと思います。


例えば、「使用できるカメラが存在していない場合」と「読み取りに成功した場合」にイベントハンドラを設けたい時は、以下のようにします。

テンプレート側

<zxing-scanner
    (camerasNotFound)="onCamerasNotFound()"
    (scanSuccess)="onScanSuccess($event)">
</zxing-scanner>

ロジック側

onCamerasNotFound(): void {
    console.log('cameras not found.')
}

onScanSuccess(result: string): void {
    console.log('scan success:', result)
}

ここで、カメラが起動するまでの流れの最後の処理(autostartScanner())をもう一度見てみます。

/**
 * Starts the scanner with the back camera otherwise take the last
 * available device.
 */
autostartScanner(devices) {
    const matcher = ({ label }) => /back|trás|rear|traseira|environment|ambiente/gi.test(label);
    // select the rear camera by default, otherwise take the last camera.
    const device = devices.find(matcher) || devices.pop();
    if (!device) {
        throw new Error('Impossible to autostart, no input devices available.');
    }
    this.device = device;
    // @note when listening to this change, callback code will sometimes run before the previous line.
    this.deviceChange.emit(device);
    this.isAutostarting = false;
    this.autostarted.next();  // <- ここ!
}

最後に、autostartedイベントが発火していますね。

これを、コンポーネント側で検知するようにしてあげればよさそうです。

テンプレート側

<zxing-scanner
    [(device)]="currentDevice"
    (camerasFound)="onCamerasFound($event)"
    (autostarted)="onAutoStarted()">
</zxing-scanner>

ロジック側

availableDevices: MediaDeviceInfo[];
currentDevice: MediaDeviceInfo = null;

...

onCamerasFound(devices: MediaDeviceInfo[]): void {
    // 使用できるカメラを取得
    this.availableDevices = devices;
}

onAutoStarted(result: string): void {
    const userAgent = navigator.userAgent.toLowerCase();
    let iosVersionInfo = userAgent.match(/iphone os ([\d]+)_([\d]+)_([\d]+)/);
    if (!iosVersionInfo) {
        iosVersionInfo = userAgent.match(/iphone os ([\d]+)_([\d]+)/);
    }
    const iosVersion = iosVersionInfo?.[1];

    if (iosVersion === '12') {
        // iOS12の場合は、配列の最初のカメラを使用する
        this.currentDevice = this.availableDevices?.[0];
    }
}

実際に、iOS12の場合は背面カメラを起動するよう設定し直すサンプルにしてみました。


まず、camerasFoundで使用できるカメラの情報が渡ってきますので、変数に控えます。

テンプレート側にある[(device)]プロパティに使用したいカメラを設定してあげるとカメラが切り替えられるので、UserAgentからiOS12と判断できる場合は配列の最初のカメラを設定するようにしました。


iOS12ではない場合はライブラリ側で自動起動したカメラが設定されますので、ピンポイントな修正が可能です。



INFO

最新版(Ver.3.2.0)では、this.autostarted.next()initAutostartOn()内に処理が移っていました。

紹介した処理の流れは大きく変わっていないと思うので、お使いのバージョンと比較してみてください。



以上が、今回のトラブルシューティングの内容でした。

どなたかの参考になれば嬉しいです🦓


0 件のコメント:

コメントを投稿