LoginSignup
9
11

More than 5 years have passed since last update.

[Signagify 4] Chromecast の Custom Receiver アプリで独自アプリ作ってみる

Last updated at Posted at 2015-12-17

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

内容は17日目の続きです。
http://qiita.com/vanx2/items/3c20bf8e4111da9eb68d

前回簡単なChromecastアプリを作って動画再生してみましたが、今回はReceiverアプリを自分で実装してみます。

Receiverアプリ

今回はCustom Receiverアプリを使って独自でReceiverアプリを実装します。
シンプルなHTMLに動画を表示します。
SDKを読み込み、メディアの管理をするMediaManagerとReceiverアプリとしてSenderと通信するcastReceiverManagerを使います。

index.html
<!DOCTYPE html>
<html>
<head>
  <!-- SDK 読み込み -->
  <script type="text/javascript" src="//www.gstatic.com/cast/sdk/libs/receiver/2.0.0/cast_receiver.js"></script>
</head>
<body>
  <!-- 動画表示エレメント -->
  <video id="vid" />
  <script type="text/javascript">
    window.onload = function() {

      // ログレベルを設定
      cast.receiver.logger.setLevelValue(cast.receiver.LoggerLevel.DEBUG);

      // MediaManager でコントロールしたいvideoエレメントを取得
      window.mediaElement = document.getElementById('vid');
      // MediaManager の開始
      window.mediaManager = new cast.receiver.MediaManager(window.mediaElement);

      // CastReceiverManager を取得
      window.castReceiverManager = cast.receiver.CastReceiverManager.getInstance();
      // CastReceiverManager の開始
      castReceiverManager.start();

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

MediaManager

動画再生などの基本的な動作を行います。イベントをオーバーライドすることで処理を加える事ができます。

CastReceiverManager

Senderアプリとの通信をします。イベントをオーバーライドすることで処理を加える事ができます。

上記で作ったReceiverアプリをGoogle Driveなどのhttpsでアクセスできるところで公開し、「Google Cast SDK Developer Console」にそのURLを登録します。登録するとAppIdが発行されます。SenderアプリからReceiverアプリを起動させるときにこのAppIdを使います。

Senderアプリ

それではSenderアプリからChromecastへ上記のReceiverアプリを実行するようリクエストし、起動後動画再生をリクエストしてみます。

前回はcastv2-clientというモジュールを使いましたが、今回はより抽象度の低いcastv2を使います。
https://github.com/thibauts/node-castv2

castv2は、SenderからChromecastの8009ポートにTLSで接続してお互いにメッセージを投げ合います。
https://github.com/thibauts/node-castv2/blob/master/README.md

メッセージはnamespaceで定義されたチャンネルを通して投げられます。
例)接続の場合のnamespaceurn:x-cast:com.google.cast.tp.connection

メッセージはJSON形式で記述されます。
例)接続の場合のメッセージ:{TYPE: "CONNECT"}

それでは実際にやってみましょう。

インストールしてー

$ npm i castv2

スクリプト書いてー

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

  client = new Client();

  // TLSで接続
  client.connect(host, function() {

    // namespace ごとにチャンネルを作る
    // 接続系
    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');

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

    // connectionチャンネルでメッセージを受け取った時の処理
    connection.on('message', function(data, broadcast) {
    });

    // TLSでつながったコネクション上で接続のメッセージを投げる
    connection.send({ type: 'CONNECT' });

    // ハートビートのピンポンを開始
    setInterval(function() {
      // 5秒おきに Chromecast へ PING を送る(PONG が返ってくる)
      heartbeat.send({ type: 'PING' });
    }, 5000);

    // Receiverアプリを起動
    receiver.send({ type: 'LAUNCH', appId: appID, requestId: 5 });

    // Receiverアプリからメッセージを受け取った時の処理
    receiver.on('message', function(data, broadcast) {
      // Receiverアプリ起動時に requestId: 5 を指定したので、
      // その返事として返って来たメッセージだったら
      if (data.requestId == 5) {
        // 返って来た RECEIVER_STATUS をログ出力
        console.log(data.status.applications[0]);
        // 今後送るリクエストで RECEIVER_STATUS の transportId を使うので保管しておく
        transportId = data.status.applications[0].transportId;

        // 送られてきた transportId を destinationId としてセットして再度接続
        connection.destinationId = transportId;
        connection.sourceId = clientId;
        connection.send({ type: 'CONNECT' });

        // 同様に transportId を destinationId としてセットして
        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: [],
        });
      }
    });
  });

DEBUG=* node hoge.jsと実行することで、castv2が送受信のログを出してくれます。

$ DEBUG=* node sender.js
  castv2 connecting to 192.168.xx.63:8009 ... +0ms
  castv2 connected +191ms
  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"} +229ms
  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":"085B7D2E-A1AE-4205-B6B3-C891D7438A77","statusText":"ac","transportId":"web-19"}],"isActiveInput":true,"isStandBy":false,"volume":{"level":1.0,"muted":false}},"type":"RECEIVER_STATUS"} +881ms
{ appId: 'D86E10F8',
  displayName: 'ac',
  namespaces:
   [ { name: 'urn:x-cast:com.google.cast.media' },
     { name: 'urn:x-cast:com.oreore.message' } ],
  sessionId: '085B7D2E-A1AE-4205-B6B3-C891D7438A77',
  statusText: 'ac',
  transportId: 'web-19' }
  castv2 send message: protocolVersion=0 sourceId=client-1 destinationId=web-19 namespace=urn:x-cast:com.google.cast.tp.connection data={"type":"CONNECT"} +5ms
  castv2 send message: protocolVersion=0 sourceId=client-1 destinationId=web-19 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-19 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} +3s
  castv2 send message: protocolVersion=0 sourceId=sender-0 destinationId=receiver-0 namespace=urn:x-cast:com.google.cast.tp.heartbeat data={"type":"PING"} +24ms
  castv2 recv message: protocolVersion=0 sourceId=receiver-0 destinationId=sender-0 namespace=urn:x-cast:com.google.cast.tp.heartbeat data={"type":"PONG"} +22ms
  castv2 recv message: protocolVersion=0 sourceId=web-19 destinationId=* namespace=urn:x-cast:com.google.cast.media data={"type":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"PLAYING","currentTime":0.35419,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"activeTrackIds":[],"currentItemId":1,"repeatMode":"REPEAT_OFF"}],"requestId":0} +1s
  castv2 send message: protocolVersion=0 sourceId=sender-0 destinationId=receiver-0 namespace=urn:x-cast:com.google.cast.tp.heartbeat data={"type":"PING"} +3s
  castv2 recv message: protocolVersion=0 sourceId=receiver-0 destinationId=sender-0 namespace=urn:x-cast:com.google.cast.tp.heartbeat data={"type":"PONG"} +48ms
  castv2 recv message: protocolVersion=0 sourceId=web-19 destinationId=* namespace=urn:x-cast:com.google.cast.media data={"type":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"PLAYING","currentTime":4.245333,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"activeTrackIds":[],"currentItemId":1,"repeatMode":"REPEAT_OFF"}],"requestId":0} +1s
  castv2 recv message: protocolVersion=0 sourceId=web-19 destinationId=* namespace=urn:x-cast:com.google.cast.media data={"type":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"PLAYING","currentTime":5.765537,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"activeTrackIds":[],"currentItemId":1,"repeatMode":"REPEAT_OFF"}],"requestId":0} +2s
  castv2 send message: protocolVersion=0 sourceId=sender-0 destinationId=receiver-0 namespace=urn:x-cast:com.google.cast.tp.heartbeat data={"type":"PING"} +877ms
  castv2 recv message: protocolVersion=0 sourceId=receiver-0 destinationId=sender-0 namespace=urn:x-cast:com.google.cast.tp.heartbeat data={"type":"PONG"} +8ms
  castv2 recv message: protocolVersion=0 sourceId=web-19 destinationId=* namespace=urn:x-cast:com.google.cast.media data={"type":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"PLAYING","currentTime":6.458583,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"activeTrackIds":[],"currentItemId":1,"repeatMode":"REPEAT_OFF"}],"requestId":0} +1s

ってなかんじで動画が再生されましたかー?

{ reason: 'NOT_FOUND', requestId: 1, type: 'LAUNCH_ERROR' }
と返ってくる場合は、指定したAppIdが無いわって怒ってるので、登録されたReceiverアプリのAppIdと間違ってないか確認して、間違って無ければ一度Chromecastを再起動してみてください。

実装のポイント

処理の流れに沿ってどういうメッセージをやりとりしているのか順に見ていきます。

接続

TLSで接続し、connectionチャンネルで{ type: 'CONNECT' }を投げるのですが、実際には下記のようなメッセージが送られます。

protocolVersion=0
sourceId=sender-0
destinationId=receiver-0
namespace=urn:x-cast:com.google.cast.tp.connection
data={"type":"CONNECT"}

protocolVersionは今のところ決め打ちで0をつけています。

sourceIdは最初sender-[Number]を指定しているようです。後ほどClient-[Number]で上書くので[Number]は特に乱数や重複など気にせず0固定にしています。

destinationIdReceiver-[Number]を指定し、後ほどReceiverアプリから返ってくるtransportIdで上書きするので0固定にしています。

namespaceurn:x-cast:com.google.cast.tp.connectionというチャンネルを通してdata={"type":"CONNECT"}というデータを送っていることがわかりますね。

接続が完了するとreceiverチャンネルで下記メッセージが送られてきます。

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"
}

sourceIdにはさきほどCONNECTを送るときに指定したdestinationIdがついてきます。

destinationId*となっているのでブロードキャストなメッセージだということがわかります。

namespaceurn:x-cast:com.google.cast.receiverというチャンネルを通して
JSONデータが送られてきています。

これはRECEIVER_STATUSということで簡単なReceiverアプリのステータスが返って来ていますが詳細なステータスは載っていません。{"type": "GET_STATUS","requestId": [Number]}を送ると詳細なRECEIVER_STATUSが返って来ます。Receiverアプリを実行するLAUNCHを送った際にも同様の詳細なRECEIVER_STATUSが返ってくるので後ほどそこで中身を見てみます。

メッセージに問題があるか、Chromecast上でエラーが発生した場合は{"type":"CLOSE"}が返って来ます。直前にエラーメッセージが送られてくる場合もあります。

接続の維持

ハートビートチャンネルでピンポンすることで接続を維持しています。

SenderからPING

protocolVersion=0 
sourceId=sender-0 
destinationId=receiver-0 
namespace=urn:x-cast:com.google.cast.tp.heartbeat 
data={"type":"PING"}

ChromecastからPONG

protocolVersion=0 
sourceId=receiver-0 
destinationId=sender-0 
namespace=urn:x-cast:com.google.cast.tp.heartbeat 
data={"type":"PONG"}

アプリの実行

receiverチャンネルでLAUNCHを投げます。

protocolVersion=0 
sourceId=sender-0 
destinationId=receiver-0 
namespace=urn:x-cast:com.google.cast.receiver 
data={
  "type":"LAUNCH",
  "appId":"D86E10F8",
  "requestId":5
}

requestIdにはリクエストごとに固有のIDをつけます。Receiverが返してくるRECEIVER_STATUSrequestIdがくっついてくるのでそれを元にどのリクエストに対するメッセージなのか判断します。

Receiverアプリのステータス

Receiverアプリが起動するとRECEIVER_STATUSが返って来ます。

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":"085B7D2E-A1AE-4205-B6B3-C891D7438A77",
          "statusText":"ac",
          "transportId":"web-19"
    }],
    "isActiveInput":true,
    "isStandBy":false,
    "volume":{
      "level":1.0,
      "muted":false}},
  "type":"RECEIVER_STATUS"
}

statusの中のAppIDでどのアプリが動いているかわかります。

アプリ AppID
BackDrop E8C28D3C
YouTube CC1AD845
Mirroring 0F5096E8

ここで大事なのはtransportIdsessionIdです。transportIdは今後そのアプリに対してメッセージを投げる際にdestinationIdに指定します。sessionIdはアプリの停止時などに指定します。

アプリを起動せずにReceiverのステータスを取得したい場合はGET_STATUSを送ります。

protocolVersion=0 
sourceId=sender-0 
destinationId=receiver-0 
namespace=urn:x-cast:com.google.cast.receiver 
data={"type":"GET_STATUS","requestId":1}

再接続

ここで、さきほど受け取ったtransportIdを使って再接続(TLSでの接続はしたままでCONNECTをもう一度投げる)をしないとReceiverアプリとの通信ができません。

protocolVersion=0 
sourceId=client-1 
destinationId=web-19 
namespace=urn:x-cast:com.google.cast.tp.connection 
data={"type":"CONNECT"} 

sourceIdにはclient-[Number]でSenderが割り振ったIDを指定します。
destinationIdにさきほど受け取ったtransportIdを指定します。

動画の再生

これでようやくReceiverアプリ上のMediaManagerにメッセージを送ることができるようになります。

protocolVersion=0 
sourceId=client-1 
destinationId=web-19 
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":[]} 

mediacontentIdに再生したい動画のURLを指定します。contentId以外は任意です。

メディアのステータス

動画の再生が始まるとMediaManagerMEDIA_STATUSが返って来ます。

protocolVersion=0 
sourceId=web-19 
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
}

LOADを送った際につけたrequestId: 9がついてきていますね。

一旦それぞれの細かいプロパティの解説は割愛します。
https://developers.google.com/cast/docs/reference/messages

repeatMode: "REPEAT_OFF"というのがあるのでLOAD時にrepeatMode: "REPEAT_SINGLE"などを指定するとループ再生するかと思いきや、うまく動作しませんでした(2015/12/22現在。そのうち動くかも)。

以前はitemsってなかった気がするんだけど、Chromecast2で提供され始めている"Fast Play"機能のために動画のキューイングができるよう対応されたのかな。

ReceiverアプリのDebug

Chromeで9222ポートにアクセスしてRemote Debugging (AppEngine)を開きます。
最初は何も表示されていませんがアドレス欄のシールドをクリックして「安全でないスクリプトを読み込む」を押すとデバッグ情報が出るようになります。

Chromecastで起動しているアプリが変わるとデバッグ画面を再度開き直す必要があります。

詳細はこちら
https://developers.google.com/cast/docs/debugging


ぜーはー自分でnamaspace定義してメッセージの送受信までやりたかったけどもうすぐAdvent Calendarの公開時間なので次回また、、、あれば、、、

コメントもらえたらやる気出して書くかもですv

9
11
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
11