[signagify 5] Receiverアプリのカスタマイズと独自の namespace でメッセージ送信

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

内容は2015年（去年ｗ）の１８日目の続きです。
http://qiita.com/vanx2/items/2f8a55177e55f5b05179

前回作ったReceiverアプリとSenderアプリに手を加えて、独自で定義した namespace 経由で独自のメッセージを送ってみます。また、Receiverアプリのカスタマイズ方法を学ぶために動画再生時に処理を加えてみます。

# Receiverアプリ

前回からの変更点は以下

- メッセージを表示するdivエレメントを設ける
- 動画ロードのリクエストが来たらどのイベントが呼ばれたか上記メッセージ欄に追記
- 動画がロードされたらどのイベントが呼ばれたか上記メッセージ欄に追記
- Senderアプリからメッセージを受け取ったらメッセージを上記メッセージ欄に追記

```html:index.html
<!DOCTYPE html>
<html>
<head>
  <script type="text/javascript" src="//www.gstatic.com/cast/sdk/libs/receiver/2.0.0/cast_receiver.js"></script>
</head>
<body>
  <!-- メッセージを表示するエリア -->
  <div id='message' style="background-color:white">x</div>
  <video id="vid" />
  <script type="text/javascript">
    window.onload = function() {

      cast.receiver.logger.setLevelValue(cast.receiver.LoggerLevel.DEBUG);

      window.mediaElement = document.getElementById('vid');

      // videoエレメントのloadstartイベントで実行される処理を追加
      window.mediaElement.addEventListener('loadstart', function(e) {
        // 呼びだされた時にメッセージエリアに文字列追加
        getElementById("message").innerHTML .= "[loadstart]";
      });

      window.mediaManager = new cast.receiver.MediaManager(window.mediaElement);
      
      // mediaManager の onLoad イベントをクロージャーでオーバーライドして
      // LOAD リクエストが来た時に実行される処理を追加
      window.mediaManager.onLoad = (function() {
        // 元の処理を退避しておく
        var orignalEvent = mediaManager.onLoad;
        // 追加する処理を記述
        return function(event) {
          // 呼びだされた時にメッセージエリアに文字列追加
          getElementById("message").innerHTML .= "[onLoad]";
          // 退避しておいた元のイベントを実行
          orignalEvent(event);
        }
      }());

      window.castReceiverManager = cast.receiver.CastReceiverManager.getInstance();
      
      // 独自メッセージで使うnamespaceを指定してメッセージバスを取得
      window.customMessageBus = castReceiverManager.getCastMessageBus('urn:x-cast:com.oreore.message');
      // メッセージを受け取った場合の処理
      customMessageBus.onMessage = function(event) {
        // 受け取ったメッセージを画面に表示
        getElementById("message").innerHTML .= event.data;
        // 送信元に受け取ったメッセージをそのまま返す
        customMessageBus.send(event.senderId, event.data);
        // 受け取ったメッセージを全てのSenderにブロードキャスト
        customMessageBus.broadcast(event.data);
      }

      castReceiverManager.start();

    };
  </script>
</body>
</html>
```

`customMessageBus`を記述してから`castReceiverManager.start();`を呼ぶようにしてください。`start`後だと反映されず激ハマリしました。。。


#Senderアプリ

CastV2の仕様がわかるように抽象化せずにつらつらと書いてみます。
前回からの変更点は以下

- 独自のnamespaceを定義
- 動画を再生したあとで `MEDIA_STATUS` が返って来たら独自メッセージを送る
- Receiverアプリから独自メッセージが送られてきたらログに出力

```js:sender.js
var Client = require('castv2').Client;
var host = '192.168.1.64'; // Chromecastのアドレス
var appID = 'D86E10F8'; // Receiver アプリの appID

  client = new Client();
  client.connect(host, function() {

    var connection = client.createChannel('sender-0', 'receiver-0', 'urn:x-cast:com.google.cast.tp.connection', 'JSON');
    var heartbeat  = client.createChannel('sender-0', 'receiver-0', 'urn:x-cast:com.google.cast.tp.heartbeat', 'JSON');
    var receiver   = client.createChannel('sender-0', 'receiver-0', 'urn:x-cast:com.google.cast.receiver', 'JSON');
    var media      = client.createChannel('sender-0', 'receiver-0', 'urn:x-cast:com.google.cast.media', 'JSON');
    // 独自のチャンネルを定義
    var original   = client.createChannel('sender-0', 'receiver-0', 'urn:x-cast:com.oreore.message', 'JSON');

    var clientId   = 'client-1';
    var transportId = '';

    connection.on('message', function(data, broadcast) {
    });

    connection.send({ type: 'CONNECT' });

    setInterval(function() {
      heartbeat.send({ type: 'PING' });
    }, 5000);

    receiver.send({ type: 'LAUNCH', appId: appID, requestId: 5 });

    receiver.on('message', function(data, broadcast) {
      if (data.requestId == 5) {
        transportId = data.status.applications[0].transportId;

        connection.destinationId = transportId;
        connection.sourceId = clientId;
        connection.send({ type: 'CONNECT' });

        media.destinationId = transportId;
        media.sourceId = clientId;

        media.send({
          requestId: 9,
          type: 'LOAD',
          media: {contentId: "http://commondatastorage.googleapis.com/gtv-videos-bucket/big_buck_bunny_1080p.mp4",
                  streamType: "buffered",
                  contentType: "video/mp4",
                  metadata: null,
                  duration: 596,
                  customData: null},
          autoplay: true,
          currentTime: 0,
          customData: null,
          activeTrackIds: [],
        });
      }
    });
    media.on('message', function(data, broadcast) {
      // 動画再生リクエストのレスポンスとして MEDIA_STATUS が返って来たら
      if (data.requestId == 9) {
        // 受け取った transportId を destinationId としてセットして
        original.destinationId = transportId;
        original.sourceId = clientId;
        // 独自メッセージを投げる
        original.send({type: 'text', message: 'hogehoge' });
      }
    });
    original.on('message', function(data, broadcast) {
      // Chromecast から送られてきた独自メッセージをログログ
      console.log(data, broadcast);
    });
  });

```

実行！

```shell-session
$ DEBUG=* node sender.js
  castv2 connecting to 192.168.x.64:8009 ... +0ms
  castv2 connected +262ms
  castv2 send message: protocolVersion=0 sourceId=sender-0 destinationId=receiver-0 namespace=urn:x-cast:com.google.cast.tp.connection data={"type":"CONNECT"} +2ms
  castv2 send message: protocolVersion=0 sourceId=sender-0 destinationId=receiver-0 namespace=urn:x-cast:com.google.cast.receiver data={"type":"LAUNCH","appId":"D86E10F8","requestId":5} +4ms
  castv2 recv message: protocolVersion=0 sourceId=receiver-0 destinationId=* namespace=urn:x-cast:com.google.cast.receiver data={"requestId":0,"status":{"isActiveInput":true,"isStandBy":false,"volume":{"level":1.0,"muted":false}},"type":"RECEIVER_STATUS"} +221ms
  castv2 recv message: protocolVersion=0 sourceId=receiver-0 destinationId=* namespace=urn:x-cast:com.google.cast.receiver data={"requestId":5,"status":{"applications":[{"appId":"D86E10F8","displayName":"ac","namespaces":[{"name":"urn:x-cast:com.google.cast.media"},{"name":"urn:x-cast:com.oreore.message"}],"sessionId":"CCF4FCE6-885C-46C3-95D1-76EEC6813A9C","statusText":"ac","transportId":"web-48"}],"isActiveInput":true,"isStandBy":false,"volume":{"level":1.0,"muted":false}},"type":"RECEIVER_STATUS"} +1s
{ appId: 'D86E10F8',
  displayName: 'ac',
  namespaces:
   [ { name: 'urn:x-cast:com.google.cast.media' },
     { name: 'urn:x-cast:com.oreore.message' } ],
  sessionId: 'CCF4FCE6-885C-46C3-95D1-76EEC6813A9C',
  statusText: 'ac',
  transportId: 'web-48' }
  castv2 send message: protocolVersion=0 sourceId=client-1 destinationId=web-48 namespace=urn:x-cast:com.google.cast.tp.connection data={"type":"CONNECT"} +5ms
  castv2 send message: protocolVersion=0 sourceId=client-1 destinationId=web-48 namespace=urn:x-cast:com.google.cast.media data={"requestId":9,"type":"LOAD","media":{"contentId":"http://commondatastorage.googleapis.com/gtv-videos-bucket/big_buck_bunny_1080p.mp4","streamType":"buffered","contentType":"video/mp4","metadata":null,"duration":596,"customData":null},"autoplay":true,"currentTime":0,"customData":null,"activeTrackIds":[]} +0ms
  castv2 recv message: protocolVersion=0 sourceId=web-48 destinationId=* namespace=urn:x-cast:com.google.cast.media data={"type":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"BUFFERING","currentTime":0,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"activeTrackIds":[],"media":{"contentId":"http://commondatastorage.googleapis.com/gtv-videos-bucket/big_buck_bunny_1080p.mp4","streamType":"buffered","contentType":"video/mp4","metadata":null,"duration":596.501333,"customData":null},"currentItemId":1,"items":[{"itemId":1,"media":{"contentId":"http://commondatastorage.googleapis.com/gtv-videos-bucket/big_buck_bunny_1080p.mp4","streamType":"buffered","contentType":"video/mp4","metadata":null,"duration":596.501333,"customData":null},"autoplay":true,"activeTrackIds":[],"customData":null}],"repeatMode":"REPEAT_OFF"}],"requestId":9} +2s
  castv2 send message: protocolVersion=0 sourceId=client-1 destinationId=web-48 namespace=urn:x-cast:com.oreore.message data={"type":"text","message":"hogehoge"} +1ms
  castv2 recv message: protocolVersion=0 sourceId=web-48 destinationId=client-1 namespace=urn:x-cast:com.oreore.message data={"type":"text","message":"hogehoge"} +140ms
{ type: 'text', message: 'hogehoge' } false
  castv2 recv message: protocolVersion=0 sourceId=web-48 destinationId=* namespace=urn:x-cast:com.oreore.message data={"type":"text","message":"hogehoge"} +6ms
{ type: 'text', message: 'hogehoge' } true
  castv2 send message: protocolVersion=0 sourceId=sender-0 destinationId=receiver-0 namespace=urn:x-cast:com.google.cast.tp.heartbeat data={"type":"PING"} +469ms
  castv2 recv message: protocolVersion=0 sourceId=receiver-0 destinationId=sender-0 namespace=urn:x-cast:com.google.cast.tp.heartbeat data={"type":"PONG"} +110ms
  castv2 recv message: protocolVersion=0 sourceId=web-48 destinationId=* namespace=urn:x-cast:com.google.cast.media data={"type":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"PLAYING","currentTime":0.536066,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"activeTrackIds":[],"currentItemId":1,"repeatMode":"REPEAT_OFF"}],"requestId":0} +353ms

```

前回と同様動画が再生され、画面上部に`[onLoad][loadstart]{type: 'text', message: 'hogehoge'}`と表示されましたでしょうか。

# 実装のポイント
## 動画再生

今回Receiverアプリに加えた修正で`mediaMnager`の`onLoad`イベントで`[onLoad]`を、`video`エレメントの`loadstart`イベントで`[loadstart]`をdivエレメントに追記しています。このように動画をLoadするイベントでも`mediaMnager`のイベントと`video`エレメントのイベント両方で記述することができます。

公式サイトによると、Senderから送られるリクエストに起因する処理は`mediaMnager`に、メディアの状態変化に伴う処理は`video`エレメントのイベントに追加することが推奨されているようです。
https://developers.google.com/cast/docs/custom_receiver

オーバーライドできるイベントのプロパティは下記の通りです。
https://developers.google.com/cast/docs/reference/receiver/cast.receiver.MediaManager#properties

videoエレメントで発火するイベントは下記を参照してください。
https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Media_events

このサイトで動画をいじってみるとvideoエレメントでどんなイベントやプロパティがいつどう変わるのか把握しやすいです。
http://www.w3.org/2010/05/video/mediaevents.html

公式サイトにはどういうUIにするべきかのチェックリストがあります。
https://developers.google.com/cast/docs/design_checklist#receiver

そうです。書くのが面倒になってきたので外部リンクに頼りました。。。

## 独自メッセージ
動画を`LOAD`するリクエストを送ると、返事として`MEDIA_STATUS`が返って来ます。今回はそのメッセージを受け取ったタイミングで独自チャンネルを通して独自メッセージを投げています。

```
protocolVersion=0 
sourceId=client-1 
destinationId=web-48 
namespace=urn:x-cast:com.oreore.message 
data={"type":"text","message":"hogehoge"}
```

Receiverアプリでは受け取ったメッセージをdivエレメントに追記し、送信元へ受け取ったメッセージを返し、接続している全てのSenderへも受け取ったメッセージをブロードキャストしています。

- 送信元へ送信

```
protocolVersion=0 
sourceId=web-48 
destinationId=client-1 
namespace=urn:x-cast:com.oreore.message 
data={"type":"text","message":"hogehoge"}
```

ブロードキャストが`false`になっているログが送信元へ返されたログです。

```
{ type: 'text', message: 'hogehoge' } false
```

- 全てのSenderへブロードキャスト


```
protocolVersion=0 
sourceId=web-48 
destinationId=* 
namespace=urn:x-cast:com.oreore.message 
data={"type":"text","message":"hogehoge"}
```

ブロードキャストが`true`になっているログがブロードキャストのログです。

```
{ type: 'text', message: 'hogehoge' } true
```

# というわけで
これらを駆使すれば動画の再生で何か問題が起きた時にSenderアプリと独自でやりとりをするなんてこともできるようになりますね！

`mediaManager`を使わずに自由にTV上にHTMLで表現することもできます。
Game用のSDKも用意されています。

こういう仕組みでFirstserver社内のデジタルサイネージは運営されているのでした☆
https://github.com/vanx2/signagify