Google Homeで伝言を届けるスマホアプリを作ってみた

# やりたいこと

わが家の子供が小学生になったのですが、あまりに忘れ物が多すぎて心配しています。子供が家をでるときに「給食袋持った？」とか「今日は天気予報が雨だから傘を持ってね！」とか言ってフォローしてあげたいのですが、私が会社に家をでる時間の方が早いため、実現できません。
そこで、最近買ったGoogle Homeを使って解決したいと思います。
やりたいことはこんなかんじです。

 * スマホで伝言を録音し、Google Homeで伝言を再生させる
 * 伝言を再生する時刻は指定できる
 * 伝言を聞き逃した時のために、繰り返し再生できる
 * 「伝言ある？」とGoogle Homeに聞いたときに再生する「オンデマンド再生」もできる

<img width="840" alt="やりたいこと" src="https://qiita-image-store.s3.amazonaws.com/0/160451/b67f62e5-077d-f4fa-1172-592f19d3e924.png">

メッセージをGoogle Homeの人工音声で読み上げるだけならばQiita記事「[LINEに送信したメッセージを、Google Homeで読み上げ、家族に通知](https://qiita.com/daaaaaasuke/items/5e95983166667094f42f)」など既に実現できているようですが、Google Homeの人工音声ではなく、パパの声で伝言を残したかったので、アプリを自作することにしました。

# 構成

システム構成は下のような感じにしました。

<img width="825" alt="スクリーンショット 2018-02-04 1.16.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/160451/c30d1e83-6afb-c66f-6761-f7ac756119a2.png">

スマホに音声を録音すると、以下の流れでGoogleHomeにて再生されます。

 1. 音声ファイルをAWS S3にアップロード
 2. 音声ファイルに関するメタデータ(後述)をFirebase Realtime Databaseに書き込み
 3. ラズパイ上のNodeJSプログラムがFirebaseへの書き込みを検知
 4. 再生すべき時刻がきたら、Google Homeに音声ファイルのURLを指定して再生を依頼
 5. Google Homeが指定されたURLの音声を取得し、再生

メタデータとしては以下の情報を格納しています。

 * S3にアップロードした音声ファイルのURL
 * 再生する時刻 
 * 音声の長さ
 * Push通知かオンデマンドか

# スマホアプリ開発
## React Native
我が家は私がiPhone、嫁がAndroidを使っているため両OSで動作するアプリをつくる必要がありました。そこで、JavaScriptでAndroid/iOSアプリを作れるReactNativeを利用しました。React Native自体の説明としてはQiita記事「[React Nativeとは何なのか](https://qiita.com/Iwark/items/7c27e919bb71f8b256f6)」などがあります。

ReactNativeではAndroid/iOSの知識がほとんどなくても、既存のReactNativeのライブラリを使用して開発できました。ライブラリの検索には[Native Directory](https://native.directory)というサイトがオススメです。
UIライブラリとしては[NativeBase](https://nativebase.io)が人気なようですが、今回は[react-native-material-kit](https://github.com/xinthink/react-native-material-kit)というのを使ってみました。コンポーネント数はNativeBaseなどと比べると少ないかもしれませんが、癖がなく使いやすかったです。このライブラリでつくった画面は下のようなかんじです。

![Simulator Screen Shot - iPhone 7 - 2018-02-06 at 00.36.32.png](https://qiita-image-store.s3.amazonaws.com/0/160451/0663873b-043d-a47a-a6b1-02f342550311.png)

中央のマイクボタンをタップすると録音、再タップまたは一定時間経過で録音終了です。`SELECT DATE & TIME`をタップするとDate Pickerで再生日時を決められます。よく使う時刻はプリセットとして一番下に表示され、ワンタップで時刻を指定できます。
react-native-material-kitには日時選択(Date Picker)が含まれていないので[react-native-modal-datetime-picker](https://github.com/mmazzarolo/react-native-modal-datetime-picker)を利用しました。

## 音声録音
音声の録音には[react-native-audio-player-recorder](https://github.com/Shinetechchina/react-native-audio-player-recorder)というライブラリを使いました。下のようなコードで簡単にマイクから録音することができます。

```js

import { AudioPlayer, AudioRecorder, AudioUtils,
} from 'react-native-audio-player-recorder'

// 録音準備
AudioRecorder.prepareRecordingAtPath(
　　`${AudioUtils.DocumentDirectoryPath}/voice.aac`, {
            SampleRate: 22050,
            Channels: 1,
            AudioQuality: 'Low',
            AudioEncoding: 'aac',
            AudioEncodingBitRate: 32000
      });

// 録音開始
AudioRecorder.startRecording()

// 録音終了
AudioRecorder.stopRecording()
```

[README](https://github.com/Shinetechchina/react-native-audio-player-recorder/blob/master/README.md)に従って、`react-native link`を実行することと、Info.plistとAndroidManifest.xmlにマイク仕様許可を設定することを忘れないようにしましょう。

## AWS S3への音声ファイルアップロード
録音した音声をAWSのS3にアップロードするためのライブラリは大きく２種類に分かれます。

 * JavaScript上で動くもの
 * ネイティブアプリのSDKで動くもの

今回は性能を求めなかったため、JavaScript上で動く[react-native-aws3](https://github.com/benjreinhart/react-native-aws3)というライブラリを利用しました。録音したファイルをS3にアップロードするには下のようなコードになります。

```js
import {Platform} from 'react-native'
import {RNS3} from 'react-native-aws3';
import {AudioUtils} from 'react-native-audio-player-recorder'

const S3_OPTIONS = {
  keyPrefix: "uploads/",
  bucket: "voice",
  region: "us-east-1",
  accessKey: "XXX",
  secretKey: "XXX",
  successActionStatus: 201,
};

const fileUri = `${AudioUtils.DocumentDirectoryPath}/voice.aac`;

// Android/iOSで場合分け
const uri = Platform.select({ 
  ios: fileUri,
  android: 'file://' + fileUri,
});

const file = {
  uri,
  name: `${new Date().toISOString()}.aac`,
  type: "audio/aac"
};

// AWS-S3へのアップロード
RNS3.put(file, S3_OPTIONS)
```

S3_OPTIONSにはシークレットキーを設定するので、別ファイルに移すなどし、うっかりGitなどで公開しないように注意しましょう。
react-native-audio-player-recorderで録音したファイルのパスですが、Androidの場合は先頭に'file://'を付与しないとエラーになりました。react-nativeが提供する`Platform.select()`をつかってAndroid/iOSの場合分けをしています。

また、iOSのバージョンによるのかもしれませんが、アップロード通信時に証明書エラーが表示され、ファイルアップロードができませんでした。ネイティブまわりがよくわかっていないのでStackOverflowの事例をいろいろ試した結果、`node_modules/react-native/Libraries/Network/RCTHTTPRequestHandler.mm`の`#pragma mark - NSURLSession delegate`という行の下に、下記のコードを追加すれば動くようになりました。

```objc

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
      completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
}
```

動くようにはなりましたが、正直何をやっているのかはわかってません。だれか教えてください。

## Firebase Realtime Databaseへのメタデータ格納

Firebaseを利用するためのライブラリもAWS-S3と同様に、ネイティブのSDKを利用するものと、JavaScriptのSDKを利用するものに分かれます。例えば、ネイティブのSDKを利用するライブラリとしてはreact-native-firebaseなどがあり、Qiita記事「[React Native Firebaseについて](https://qiita.com/koki_cheese/items/e9f165563b3dadb0da03)」などで紹介されています。
今回は性能を求めないので、JavaScriptのSDKを利用することにしました。JavaScriptのライブラリをつかう場合はWebと同様なので、[公式のドキュメント](https://firebase.google.com/docs/database/web/start?hl=ja)に従えば特に困ることはありませんでした。


# RaspberryPi側アプリ (Node.js)

Node.jsでもWebと同様にFirebaseのJavaScript SDKを利用することができます。Node.jsからFirebase Realtime Databaseの変更を監視し、新しいメタデータの登録を検知すると、メタデータの指定時刻に、[Google-Home-Notifier](https://github.com/noelportugal/google-home-notifier)で音声ファイルのURLを指定してGoogle Homeに再生させています。
指定時刻に再生するのに[Node-Schedule](https://github.com/node-schedule/node-schedule)というライブラリを利用しました。このライブラリを使うと、下のように指定時刻に任意の関数を実行できます。

```js
Schedule.scheduleJob(new Date('2018-02-06T09:00:00'), 
   () => notifyWithGoogleHome());
```

また、Google-Homeに「もう一度再生して」と話すとIFTTT連携でFirebase Realtime Databaseにトリガーとなるデータを書き込みむようにし、Node.jsで同様に検知して伝言をもういちど再生するようにしました。
オンデマンド再生も似たような仕組みです。

## Google-Home-Notifierは非同期実行
Node.js側の実装では、Google-Home-Notifierによる音声再生が非同期実行であることに悩みました。非同期実行のため、同じ時刻に複数の伝言があると、うまく動作しないのです（１つしか再生されない）。
そこで、音声録音時に伝言の長さ(再生時間)を計測してメタデータとし、Node.jsでは再生後にこの再生時間分のウェイト(sleep)を入れることで解決しました。

```js
const sleep = interval => new Promise(resolve => 
    setTimeout(() => resolve(), interval));

const notify = async (url, duration) => {
  googlehome.play(url);
  await sleep(duration);
}
```

# 完成！

以上は要点だけですが、こんな感じで無事にやりたかったことを実現できました:blush:
子供へのフォローだけでなく、嫁への「今日は遅くなるから晩ゴハンいらないよ」も、LINEではなく自分の声で届けられて、より家庭が円満になった気がします。
[最近作ったこちらの工作](https://qiita.com/vimyum/items/c81f08df9ff31bc446ee)は家族の理解を得られず悲しんでいましたが、このアプリは家族から好評でとても嬉しいです♪