google-home-notifier で スピーカーグループを喋らせる


google-home-notifier で スピーカーグループを喋らせる

Google HomeにPUSHで音声をしゃべらせたり、音声データを流し込んだりするのに定番な google-home-notifier.

少々古いプログラムということもあってか、複数台のGoogle Homeがあると、色々と問題があります。
自分がひっかかっていない部分は検証していませんが、ざっとプログラムを見た感じだと

  • Google Homeが2台以上あった場合の動作が不安定
  • スピーカーグループに対しての操作ができない
  • Google Homeの名前の付け方によっては、うまく送れないことがある

などがあります。

Google Homeが2台以上あった場合の動作が不安定

google-home-notifierでは、mdnsを使ってGoogle Homeを探索しています。該当部分は

google-home-notifier.js
var mdns = require('mdns');
var deviceAddress;
...
var play = function(mp3_url, callback) {
  if (!deviceAddress){
    browser.start();
    browser.on('serviceUp', function(service) {
      console.log('Device "%s" at %s:%d', service.name, service.addresses[0], service.port);
      if (service.name.includes(device.replace(' ', '-'))){
        deviceAddress = service.addresses[0];
        getPlayUrl(mp3_url, deviceAddress, function(res) {
          callback(res);
        });
      }
      browser.stop();
    });
  }else {
    getPlayUrl(mp3_url, deviceAddress, function(res) {
      callback(res);
    });
  }
};

このコードだと、最初に見つかったGoogle Homeが探索しているものと違う名前だった場合でもbrowser.stop()が実行され、探索が打ち切られてしまいます。これによって、実際には存在するGoogle Homeに要求が送れないということが発生する可能性があります。
逆に、'serviceUp'なデバイスが見つからない場合は、探し続けることになります。
if(!deviceAddress)のelseの処理を見ると、一度見つけたGoogle Homeは2回めからは問答無用に利用しています。
仕様なのかもしれませんが、ちょっと癖があります。

mdns-jsにあるサンプルなどでも、基本はタイマーなどを使って一定期間探索させて見つからなかった場合は探査を打ち切る処理をしていますね。
https://github.com/mdns-js/node-mdns-js/blob/master/examples/simple.js

simple.js
//stop after timeout
setTimeout(function onTimeout() {
  browser.stop();
}, TIMEOUT);

自分が実装するとしたら、毎回mdnsでサーチ、該当するもの全部に対してgetPlayUrlとcallbackを実行、10秒後にstop()とするかな。(仕様が変化します)

スピーカーグループに対しての操作ができない

Google Homeは複数のスピーカーをグループにして名前をつけることが出来ます。が、google-home-notifierではそのグループ名に対して操作しても、一つが反応するだけです。これはGoogle Home側の問題ではなく、google-home-notifier側のバグ(あるいは仕様)です。

調査したところ、Google Homeのグループは、グループのなかの一つのGoogle Homeが代表となってリクエストを受け付けていました。そして自分個別のリクエストと、グループ向けのリクエストでは違う番号のポートを開けています。

例えば、home1, home2, home3 という3つがあり、さらに3つをグループにしたhome-all があったとします。その時、

home1     192.168.1.100:8009
home2     192.168.1.101:8009
home3     192.168.1.103:8009
home-all  192.168.1.100:42107    (home1がグループの代表になっている。別ポートで受け付けている)

という様な形でリクエストを待ち受けています。なお、IP(192.168.1.XXX)はそれぞれのネットワーク環境によって変わります。が、ポートの8009は基本的には変化しないようです。グループ受付用のポート42107も可変です。(グループをいくつも作ることができるからと思われます。)

この部分の問題は、
https://github.com/noelportugal/google-home-notifier/blob/master/google-home-notifier.js

google-home-notifier.js
      if (service.name.includes(device.replace(' ', '-'))){
        deviceAddress = service.addresses[0]; //IPアドレスだけを取得、portは捨ててる
        getSpeechUrl(message, deviceAddress, function(res) {
          callback(res);
        });

この部分で、せっかくservice.port に返ってきているポート番号を拾わないことが原因です。

その後、文字列の'192.168.1.100'は deviceAddress 経由で hostとなって

google-home-notifier.js
var onDeviceUp = function(host, url, callback) {
  var client = new Client();
  client.connect(host, function() {

と、ポート番号が無いまま使われます。

ではなぜ、8009に送られているのか?それはClientのconnect
https://github.com/thibauts/node-castv2/blob/master/lib/client.js
の中で

client.js
Client.prototype.connect = function(options, callback) {
  var self = this;

  if(typeof options === 'string') {            // <-- ホストが文字列で直接書き込まれていたら
    options = {
      host: options
    };
  }

  options.port = options.port || 8009;         // <-- portが0か未設定なら 8009を使用
  options.rejectUnauthorized = false;

つまり、connectは本来、hostとportを持つオブジェクトoptionsを期待しているが、文字列としてのIPも受け付けてくれて、その時はデフォルトとして8009ポートを使うという仕様になっているということです。(個人的にはこういう実装は大嫌い)
結果として、home-all (192.168.1.100:42107)を操作しようとすると home1 (192.168.1.100:8009) が操作されてしまいます。

さて、原因がわかったところで、解決策です。
一番楽なのはgoogle-home-notifier.jsをいじってしまうことです。

google-home-notifier.js
      if (service.name.includes(device.replace(' ', '-'))){
        deviceAddress.host = service.addresses[0];         // <-- 修正
        deviceAddress.port = service.port;                 // <-- 追加
        getSpeechUrl(message, deviceAddress, function(res) {
          callback(res);
        });

これでポートが拾えました。

example.js
googlehome.device('home-all','en');

という形でグループ全体のスピーカーから音声が流れると思います。

Google Homeの名前の付け方によっては、うまく送れないことがある

のサンプルにある、

example.js
var deviceName = 'Google Home';
...
googlehome.device(deviceName,language);

ですが、このdeviceNameは後のプログラムの中で、

google-home-notifier.js
      if (service.name.includes(device.replace(' ', '-'))){
        deviceAddress = service.addresses[0];
        getSpeechUrl(message, deviceAddress, function(res) {
          callback(res);
        });

のように、検索する名前のスペースをハイフンに変換後、最初に見つかったものに対してnotifierを実行しています。
ここから推察すると、

  • 'Google Home' と 'Google-Home' の区別はできない
  • 'GoogleHome1' 'GoogleHome2' があった時、 'GoogleHome'で指定すると、どちらに送られるかはわからない(最初に応答した方に送られるので、毎回結果が安定しない)
  • 'GoogleHome1' と 'GoogleHome10' があった時、'GoogleHome1'とすると、'GoogleHome10'に送られることがある

などが想定されます。ここは素直に完全一致にしてもらった方が使いやすいと思います。