[Signagify 3] Node.js で Sender アプリ作って mDNS + CastV2 で Chromecast とお話


この記事はファーストサーバのAdvent Calendar 2015の１７日目として書きました。
http://qiita.com/advent-calendar/2015/firstserver

内容は１６日目の続きです。
http://qiita.com/vanx2/items/00dcad135b9706d199a7

前回ざっくりとしたChromecastアプリの概要を書いたので、今回は実際にアプリを作ってみようと思います。いいぞもっとやれと大変不評だったので図をやめました。

#やること
第一回で述べたとおり、スレート端末を使わずに複数台のChromecastへキャストをするためにはSDKを使わずに独自で実装する必要があります。1台のChromecastにキャストするだけなら提供されているSDKを使ったサンプルアプリを動かしてみてください。
https://developers.google.com/cast/docs/downloads

今回はSenderアプリをNode.jsで実装して、Default Media Receiverで動画を再生してみます。

#Senderアプリ
###■mDNSでChromecastを見つける
まずはネットワークに参加しているChromecastを見つけます。ChroemcastはAppleのBonjour（Zero-configuration networking）で利用されているmDNSという技術を利用しています。

mDNSはローカルネットワーク上の機器同士がサーバーを介さずにIPマルチキャストを使って名前とアドレスを交換しあう技術です。マルチキャストIPアドレス`224.0.0.251`の`5353`ポートにUDPでクエリを投げると、同じローカルネットワーク内に接続されたmDNSに対応した機器がDNSのようなレコードを返します。

それでは実際にmDNSにqueryを投げて、返って来たresponseから必要な情報を抜き出してみましょう。

Node.jsのパッケージでは`mdns`が人気なようですが、
https://www.npmjs.com/package/mdns

外部モジュール（LinuxのmDNS実装であるAvahiやmDNSResponderなど）に依存していたので、私はpure javascriptだった`multicast-dns`を利用しました。
https://www.npmjs.com/package/multicast-dns

インストールしてー

```shell-session
$ npm i multicast-dns
```

`name`が`_googlecast._tcp.local`な`PTR`レコードをリクエストするクエリ投げるスクリプト書いてー

```js:detect.js
var mdns = require('multicast-dns')()

mdns.on('response', function(response) {
  console.log('got a response packet:', response)
})

// lets query for an PTR record for '_googlecast._tcp.local'
mdns.query({
  questions:[{
    name: '_googlecast._tcp.local',
    type: 'PTR'
  }]
})
```

実行するとー

```shell-session
$ node detect.js
got a response packet: { type: 'response',
  qdcount: 0,
  ancount: 1,
  nscount: 0,
  arcount: 3,
  questions: [],
  answers:
   [ { name: '_googlecast._tcp.local',
       type: 'PTR',
       class: 1,
       ttl: 120,
       data: 'signage@ajito._googlecast._tcp.local' } ],
  authorities: [],
  additionals:
   [ { name: 'signage@ajito._googlecast._tcp.local',
       type: 'TXT',
       class: 99999,
       ttl: 4500,
       data: 'id=6b2e9253d1d818dd2f2436677a98b19a' },
     { name: 've=04.md=Chromecast.ic=/setup/icon.png.fn=signage@ajito.ca=5.st=1.bs=FA8FCA57FB52.rs=signage.signage@ajito._googlecast._tcp.local',
       type: 'SRV',
       class: 99999,
       ttl: 120,
       data: [Object] },
     { name: 'signage@ajito.local',
       type: 'A',
       class: 99999,
       ttl: 120,
       data: '192.168.xxx.51' } ] }
```
Chromecastがこんなレコードを返してきます。他の機器もクエリを投げるので実行し続けているとChromecast以外のmDNSに対応した機器もレコードを返してきます。

返してきた`PTR`レコードにのりづけされた`additionals`セクションの`type`が`A`のレコードの`data`からIPアドレスが、`name`からは名前が取得できます。
この例では`signage@ajito`という名前をつけたChromecastのIPアドレスが`192.168.xxx.51`だということがわかります。

###■Senderアプリの実装

見つけたChromecastのIPアドレスに対してcastv2プロトコルで接続し、標準で提供されているDefault media receiverを起動して動画を再生させてみます。

node-castv2-clientを利用します。
https://github.com/thibauts/node-castv2-client

インストールしてー

```shell-session
$ npm i castv2-client

# Windowsの場合は
C:> npm install castv2-client --no-optional
# らしい

```

ほぼほぼExamplesに書いてある通りですが、動画を再生して5秒後に再生時間2:00時点へと動画をスキップするように書いてみます。時代はES6だというのに牧歌的な書き方をしておりますが。。。

```js:sender.js
var Client                = require('castv2-client').Client;
var DefaultMediaReceiver  = require('castv2-client').DefaultMediaReceiver;

var content = 'http://commondatastorage.googleapis.com/gtv-videos-bucket/big_buck_bunny_1080p.mp4';
var host='192.168.1.96'; // your IP address of chromecast

var client = new Client();
// connect to Chromecast
client.connect(host, function() {
  console.log('connected! Launching DefaultMediaReceiver ...');
  // launch Default Media Receiver app
  client.launch(DefaultMediaReceiver, function(err, player) {
    if(err) {console.log(err);return;}
    console.log('app "%s" launched, loading media %s ...', player.session.displayName, content);

    player.on('status', function(status) {
      console.log('status broadcast playerState=%s', status.playerState);
    });

    // play a video
    player.load({contentId: content}, { autoplay: true }, function(err, status) {
      console.log('media loaded playerState=%s', status.playerState);

      // Seek to 2 minutes after 5 seconds playing
      setTimeout(function() {
        player.seek(2*60, function(err, status) {
          if(err) {console.log(err);return;}
          console.log('skip to 2:00 playerState=%s', status.playerState);
        });
      }, 5000);
    });
  });
});
client.on('error', function(err) {
  console.log('Error: %s', err.message);
  client.close();
});
```

実行するとー

```shell-session
$ node sender.js
connected! Launching DefaultMediaReceiver ...
app "Default Media Receiver" launched, loading media http://commondatastorage.googleapis.com/gtv-videos-bucket/big_buck_bunny_1080p.mp4 ...
status broadcast playerState=IDLE
status broadcast playerState=BUFFERING
media loaded playerState=BUFFERING
status broadcast playerState=PLAYING
skip to 2:00 playerState=BUFFERING
status broadcast playerState=BUFFERING
status broadcast playerState=PLAYING
  :
```

てな具合に ええ いきたいッスね いッスねー！イェー（以下略

Chromecastで動画は流れましたでしょうか。ネットワーク環境によってはバッファリングで時間がかかり、５秒以上経ってからスキップされるかもしれません。



というわけで。。。やっとmDNS触ってみましたが。。。Advent calenderの公開時間になってしまいそうなので今回はここまでとしてcastv2の詳細に関しては次回書いてみようと思います。