我が名はなんとか菜である!

主に技術系の記事を書きますが、ポエムも混入します。

HTML5版のニコ生プレイヤーはどのようにしてコメントを取得しているかを調べた

HTML5時代のリアルタイムデータ取得

ユーザー生放送のHTML5プレイヤー化が完了し、モダンな作りになってきたニコニコ生放送のプレイヤーですが、内部で使われている方法もそれに合わせてかなりモダンなものとなっています。
今回、Chrome拡張によるニコ生コメントビューワーを作るにあたり、どのようにリアルタイムなコメント取得を行っているのかを調べたところ、WebSocketを使っていることが分かったので、そこで得られた知見を書いていこうと思います。

WebSocketとは

大雑把に言えば、「ブラウザ上でリアルタイム通信を行う技術」です。
今までブラウザ上でチャットのようなリアルタイム(に見せかけた)通信を行うためには、一定時間ごとにサーバーに取得しに行ったり、クライアントからのリクエストをサーバーが握ったままにしておき、データ更新の必要があるタイミングでレスポンスを返すという「ロングポーリング」と呼ばれる裏技的なテクニックを用いる必要がありましたが、これらはサーバーのリソースを多く消費したり、実装が複雑になりがちといった問題がありました。
WebSocketを使うと、こういった問題をシンプルに解決することが出来ます。
(より詳しい説明は他に譲ります。てへぺろ。)

プレイヤーの通信を覗いてみる

さて、とっつきのための前提知識はおおよそ整ったかと思われるので、早速実際にニコ生のコメントを取得してみようと思います。
まず、ブラウザで適当な ユーザー生放送 を開き、開発者ツールで通信内容を覗いてみましょう。(公式放送の場合、Flash版プレイヤーが立ち上がる可能性があるため。HTML5版プレイヤーで視聴出来る放送なら公式放送でもOKです。)

(以下、全てGoogleChromeの場合とさせて頂きます。他のブラウザをご使用の場合は適宜調べて下さい。見たいものは通信内容なのでそこまで大きく操作は変わらないはず。)

GoogleChrome 66なら、F12またはCtrl+Shift+iキーで表示される開発者ツール上の Network というタブが該当します。


f:id:happo31:20180516014147p:plain


これを開いたら、一度ページを更新します。
放送を開いてから開発者ツールを開いた場合、ページを開いたときの通信が見られないからです。

さて、Nameという欄にブラウザが投げたリクエストがズラッと並んでいると思いますが、今回見たいのはWebSocketの通信だけなので、表示をフィルタリングします。
WS という部分をクリックしましょう。


f:id:happo31:20180516015339p:plain [WS] = WebSocket の略です。


すると、 websocket という行と、 {数字の羅列?audience_token=適当な文字列} のような2つだけになったかと思います。

f:id:happo31:20180516015733p:plain


このうちどちらでも良いのでクリックし、 Frames を選ぶと、晴れてWebSocketの通信内容を見られるようになります、おめでとうございます。


f:id:happo31:20180516020000p:plain
↑ Frame タブを選択


f:id:happo31:20180516020829p:plain ↑WebSocketの通信(イベントメッセージ)が見られる!!


この2つのログの違いですが、 websocket のほうを覗いてみると、コメント情報が格納されているのが分かると思います。


f:id:happo31:20180516021443p:plain


こいつがどこから来ているのかというと、Headersタブに書いてあります。


f:id:happo31:20180516021518p:plain


この放送では ws://omsg102.live.nicovideo.jp:83/websocket というコメントサーバーに接続しているようです。
ではもう一つのほうは、ということでひとつずつメッセージを覗いていくと、初めの方は放送を受信するために必要なセッション情報を受信し、たまにプレイヤー側から自発的にheartbeat(セッションを維持するための通信)を行っているようです。
通信の始めの方をよく見ると、接続すべきコメントサーバーの情報も含まれていることがわかります。


f:id:happo31:20180516022011p:plain


どうやらニコ生は、こういったメタ情報の取得にもWebSocketを使っているようです。
これに関しては完全に盲点で、始め僕はてっきり通常のAPIとのHTTP通信で取得しているものと勘違いしており、これに気がつくまでかなり時間がかかってしまいました。
ちなみに、 audience_token のほうはどこにあるのかというと、実はページのDOMの中にJSONとして埋め込まれています。


f:id:happo31:20180516022512p:plain


これがわかっていれば話は早く、JavaScriptで取得する場合は

const audience_token = JSON.parse(document.getElementById("embedded-data").getAttribute("data-props"));

として、プロパティを辿っていけばOKです。

(ブラウザを経由しない外部クライアントの場合はどうすればいいか調べきれませんでした…。http://live.nicovideo.jp/api/getplayerstatus/{放送ID} というAPIを使えばいいかなと思ったのですが、そこにそれらしき情報はありませんでした。)
(まあ、ブラウザ上で動かすという制約がない場合は直接TCPストリームでデータを取得すれば良いので、わざわざWebSocketを使う必要はないかもしれませんが)
(ちなみに、そっちの方法はすでにネット上に情報が溢れているので、そちらをご参照ください。)

実際にやってみた and 蛇足

※ここからはほぼ蛇足です。
なんで調べたかといえば、冒頭でも一瞬出しましたが、 Chrome拡張で動作するコメントビューワー が作りたかったからです。

github.com

余談ですが、このコードでは自力でWebSocketを作っていません。
アプリの生成するWebSocketのインスタンス生成をhookし、メッセージの受信イベントに相乗りすることでデータを取得しています。
こうすることで新たなWebSocketセッションを張らずに済むので、ニコニコのサーバーにも優しいのではないかと思います。

こういう仕組みにした理由としては、自前でAPIからデータを得ようと思うと自分でメッセージの生成や送信を行う必要があり、 ちゃんとハンドラ書くのめんどくさい サーバーに思わぬ負荷が掛かってしまう可能性があったからです。

…あ、hookといっても怪しい方法を取っているわけではなく、WebSocketの生成に対してこういうコードを入れているだけです。

// index.tsx
(window as any).WebSocket = new Proxy(WebSocket, {
  construct(target: any, args: any[]) {
    const ws = new target(...args);
    // 生成したWebSocketをインスタンスに保存
    pageWebsocketRepository.addWebSocket(ws);
    return ws;
  }
});

Proxy, 知らなかった…とても便利…。(教えてくれたフォロワーさんありがとうございます)

上記Chrome拡張はそのうちストアでも公開する予定なので、その際はぜひ使ってみてください。
PullRequestも歓迎しております。(とはいえまだ機能がないに等しいのですが…)

それでは。