この記事はファーストサーバの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
を使います。
<!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
で定義されたチャンネルを通して投げられます。
例)接続の場合のnamespace
:urn:x-cast:com.google.cast.tp.connection
メッセージはJSON形式で記述されます。
例)接続の場合のメッセージ:{TYPE: "CONNECT"}
それでは実際にやってみましょう。
インストールしてー
$ npm i castv2
スクリプト書いてー
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
固定にしています。
destinationId
もReceiver-[Number]
を指定し、後ほどReceiverアプリから返ってくるtransportId
で上書きするので0
固定にしています。
namespace
がurn: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
が*
となっているのでブロードキャストなメッセージだということがわかります。
namespace
がurn: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_STATUS
にrequestId
がくっついてくるのでそれを元にどのリクエストに対するメッセージなのか判断します。
##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 |
ここで大事なのはtransportId
とsessionId
です。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":[]}
media
のcontentId
に再生したい動画のURLを指定します。contentId
以外は任意です。
##メディアのステータス
動画の再生が始まるとMediaManager
のMEDIA_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