ニコ生新配信の放送をアプリで再生するための覚書き

## はじめに

2017年8月24日を境にニコニコ生放送のプレイヤーがHTML5プレイヤーに移行するようですね。

この記事では、そのHTML5プレイヤーが扱う新配信仕様の放送を、任意のアプリ上から再生するための情報をまとめていきます。紹介する内容はHTML5プレイヤーページから得られる範囲の内容になります。

実装環境を特定しない形で、さわりの部分だけ扱います。

~~なお、ニコ生でのコメント送信や受信は取り扱いません。Flash版と同じ仕様なのでググってやってください。~~


## 追記：2018/03/30 
視聴継続のハートビード処理がHTML5生放送プレイヤーでは視聴セッションWebSocketで行われているようなので記事内容も追従して修正しました。
またコメント送受信が niwavided ? という内部形式でWebScoket通信しているようなので~~こちらも別記事としてまとめる予定~~。

書きました
[ニコ生のコメント送受信をWebSocket+JSONでやる方法ざっくり解説](https://qiita.com/tor4kichi/items/4df5b11ec564bb8f8d16)

## 新配信のクライアントサイドの概要

1. 放送情報をやり取りするWebSocket（視聴セッション）を接続
* 視聴権獲得メッセージ（getpermitコマンド）の送信
* 放送情報メッセージ（currentStreamコマンド）の受信
  * 再生可能画質、再生URL、コメントサーバーURL、視聴者数コメント数、など
* 再生URLから映像の再生開始
* 視聴継続メッセージの定期的な送信
* 画質変更の送信





## 視聴セッション用 WebSocket を開始する

### まずは放送情報を生放送ページから取り出す

WebSocketで視聴セッションを開始するその前に、HTML5プレイヤーが表示される放送ページの情報が必要になります。

HTML5プレイヤーページをのHTMLドキュメントを取得するには

http://live2.nicovideo.jp/select_player/{LiveId}?player_name=leo

にアクセスして、リダイレクトするか

http://live2.nicovideo.jp/watch/{LiveId} へアクセスする際、クッキーに

* key: player_version 
* value: leo

を追加することでHTML5プレイヤーのHTMLドキュメントを取得できるはずです。

HTML5プレイヤーページにアクセスできたら、次に leoPlayerProps のデータを取り出します。

以下にleoPlayerPropsを抜粋します。


```js
var leoPlayerProps = {
    apiBaseUrl: "http://live2.nicovideo.jp/unama/api/v1/",
    staticResourceBaseUrl: "http://nicolive.cdn.nimg.jp/relive/relive_assets/2.0.125",
    programReportApiBaseUrl: "http://untnp.live.nicovideo.jp/",
    webSocketBaseUrl: "ws://a.live2.nicovideo.jp:2805/unama/wsapi/v1/watch/",
    nicolivePublicApiBaseUrl: "http://ow.live.nicovideo.jp/",
    nicolivePublicRootUrl: "http://live.nicovideo.jp/",
    broadcastId: "6272909836912",
    providerType: "community",
    relatedNicoliveProgramId: "lv303626359",
    title: unescapeHTML("&#33382;&#12371;&#12428;&#12288;&#12405;&#12376;&#12416;&#12425;&#12398;&#38609;&#35527;&#26449;"),
    openTime: 1501150973 * 1000,
    beginTime: 1501151034 * 1000,
    endTime: 1501154634 * 1000,
    serverTime: 1501151322974,
    audienceToken: "6272909836912_53842185_1501237722_a15d66ada261a1e4e771e7d3c2b3f40de0da5694",
    premiumMemberRegistration: {
        url: "https://account.nicovideo.jp/premium/register",
        premiumOrigin: "0"
    },
    externalLayout: {
        applyBrowserFullscreen: Fullscreen.applyBrowserFullscreen,
        unapplyBrowserFullscreen: Fullscreen.unapplyBrowserFullscreen,
        applyMonitorFullscreen: Fullscreen.applyMonitorFullscreen,
        unapplyMonitorFullscreen: Fullscreen.unapplyMonitorFullscreen,
        getLeoPlayerContainerElement: function() {
            return document.querySelector("[data-player-layout]");
        }
    },
    externalClient: {
        userAdNoticeClient: Relive.PlayerClient,
        allegationReportClient: Relive.PlayerClient
    },
    userStatus: {
        id: "53842185",
        isPremium: false,
        isOperator: false
    },
    coeResourcesBaseUrl: "http://nicolive.cdn.nimg.jp/live/coe/1.0.19/",
    isSocialGroupMember: false,
    socialGroup: {
        name: unescapeHTML("&#33382;&#12371;&#12428;&#12524;&#12505;&#12522;&#12531;&#12464;&#12398;&#24035;&#12288;second"),
        registrationUrl: "http://com.nicovideo.jp/motion/co2185672?ref=live2watch",
        thumbnail: "http://icon.nimg.jp/community/s/218/co2185672.jpg?1462527929"
    },
    bspComment: {
        isPermittedToPost: false,
        postApiUrl: "http://live.nicovideo.jp/api/relive/bsp/comment/lv303626359",
        defaultUserName: unescapeHTML("")
    },
    commentFiltersApiUrl: "",
    csrfToken: "",
    isCommentBanned: false,
    programState: JSON.parse(unescapeHTML("{&quot;commentLock&quot;:false,&quot;enquete&quot;:{},&quot;jump&quot;:{},&quot;permanentComment&quot;:{&quot;body&quot;:&quot;&quot;,&quot;name&quot;:&quot;&quot;,&quot;mail&quot;:&quot;&quot;},&quot;videoAd&quot;:{},&quot;payProgramTrialWatch&quot;:{},&quot;audienceCommentPosition&quot;:&quot;normal&quot;}"))
};
```

パッと見た感じはJSONですが、JSON.parse メソッドや unescapeHTML メソッドなどが含まれているJavaScriptのオブジェクトです。そのため単純なJSONパーサでは解析できません。

Web系であればjsオブジェクトとしてそのままで扱えると思いますが、クライアントアプリ等ではJSONパーサでは解析できないため、手作業で解析、またはJSONとして扱えるようにトリミングして強引にJSON解析してやる必要があります。


### 視聴セッションにWebSocketで接続

視聴セッションのWebSocket 接続で実際必要になるのは、先述のleoPlayerPropsの中で…

* broadcastId
* audienceToken
* webSocketBaseUrl

の３つになります。

WebSocketでアクセスする際のURLは

```text:変数ベースで示したURL
{webSocketBaseUrl}{broadcastId}?audience_token={audienceToken}
``` 

```text:実際の値で埋めたURL例
http://a.live2.nicovideo.jp:2805/unama/wsapi/v1/watch/6272909836912?audience_token=6272909836912_53842185_1501237722_a15d66ada261a1e4e771e7d3c2b3f40de0da5694
```

となります。






## 視聴セッションのメッセージ

視聴セッションのWebSocket通信は以下のようにJSON形式のテキストメッセージをやり取りします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/62922/b0bc3651-1008-36b7-1636-9377cfb4eb60.png)



### 視聴開始メッセージの送信

以下はクライアント側から最初に配信サーバーへ送信するメッセージの内容です。

```json:フラット
{"type":"watch","body":{"command":"getpermit","requirement":{"broadcastId":"{BroadcastId}","route":"","stream":{"protocol":"hls","requireNewStream":true,"priorStreamQuality":"low","isLowLatency":true},"room":{"isCommentable":true,"protocol":"webSocket"}}}}
```

```json:整形
{
	"type": "watch",
	"body": {
		"command": "getpermit",
		"requirement": {
			"broadcastId": "{BroadcastId}",
			"route": "",
			"stream": {
				"protocol": "hls",
				"requireNewStream": true,
				"priorStreamQuality": "low",
				"isLowLatency": true
			},
			"room": {
				"isCommentable": true,
				"protocol": "webSocket"
			}
		}
	}
}
```

{BroadcastId} には leoPlayerProps の BroadcastId を代入してください。

先の画像の上から５番目、body の右隣に currentStream がある行を確認すると動画再生用のURLが確認できるかと思います。

```
http://pc01.dmc.nico:2812/hlslive/ht2_nicolive/nicolive-production-pg2860099043952_59b2dfd7093b489681112e9378e6da1e677a91e6464b89766f124b232160d75b/master.m3u8?ht2_nicolive=53842185.sxk8ks_otmdep_242wfbhnip8j6
```

この再生URLを使ってストリーミング動画を取得、再生します。

currentStreamコマンドを受け取ったらアプリ側でイベントをトリガーして画面表示側での動画再生処理を更新する、という流れを作っておけば動的な画質変更にもそのままで対応できるはずです。


### 視聴継続メッセージの送信

視聴権獲得メッセージの後で受け取ることができる一連のメッセージに含まれる **watchinginterval** コマンド で指定された間隔（秒単位、先の画像の例では 30秒）で以下の watching メッセージを送信します。

```
{"type":"watch","body":{"command":"watching","params":["{Props.BroadcastId}","-1","0"]}
```


### 画質変更メッセージの送信

getpermitを送信後、以下のメッセージをWebSocketから送信することで対応する画質の再生URLの乗ったcurrentStreamコマンドが返送されてきます。

```json:フラット
{"type":"watch","body":{"command":"getstream","requirement":{"protocol":"hls","quality":"{quality}"}}}
```

```json:整形
{
	"type": "watch",
	"body": {
		"command": "getstream",
		"requirement": {
			"protocol": "hls",
			"quality": "{quality}"
		}
	}
}
```


qualityの内容は最初に受け取ったcurrentStreamに含まれる "qualityTypes" で示されたいずれかの文字列を指定します。

qualityの種類は、確認できたものを挙げると…

* abr（自動選択）
* high（高画質 2Mbps）
* normal（通常画質 1Mbps）
* low（低画質 384kbps）
* super_low（モバイル画質 192kbps）

の５種類になります。

通常会員でも混雑していない時間であればhighの画質が降ってくるようです。

また、時間帯によってはプレミアム会員でもhighが含まれない場合もあるようなので、決め打ちせずに受け取った画質から変更リクエストをするようにしたほうがよさそうです。

画質変更リクエストを送信後、改めて受け取ったcurrentStreamから再生URLを取り出して再生処理を行うことで画質変更が完了となります。

### 放送終了メッセージの受信

WebSocketAPIからdisconnectコマンドが送られてきます。メッセージのparamsから終了時刻と終了理由がとれます。





## 新配信の再生を実装する

新配信に用いられるHLS形式はffmpegでサポートされているため、基本的にはffmpegにDLから動画ストリーム生成まで任せればアプリ側の実装は容易かと思います。

（自分がそうしているため詳細な説明はできないので割愛します）


追記：2018/03/30
Windows10であればUniversalWindowsPlatformのAdaptiveMediaSourceを利用するとffmpeg無しでHLSの再生が可能です。


## コメントの送受信に対応する

別記事にまとめています

[ニコ生のコメント送受信をWebSocket+JSONでやる方法ざっくり解説](https://qiita.com/tor4kichi/items/4df5b11ec564bb8f8d16)


# さいごに

leoPlayerPropsをHTMLドキュメントからではなくAPI経由で取得する方法あったらコメントで教えて頂けると助かります。（ないかな？）


新配信については、Flash版と比べると、Socketで動的な情報のやり取りがまとめられている分、より簡便に扱えるようになっていると思いました。

放送のレイテンシも利用者側としては不便は感じないほどになってる、という感想です。中の人すごい。

Flashプレイヤーが2020年にはお別れとなるようですし、ニコニコの動画や生放送が2017年というタイミングで早々？に移行していくという流れは歓迎したいものですね。






# 宣伝

この記事内容を実際に実装している「Hohoema」というWindows10向けのニコニコ動画アプリを作りました、使って（迫真

ダウンロードページ（マイクロソフトストア）
https://www.microsoft.com/ja-jp/store/p/hohoema/9nblggh4rxt6

ちなみに無料だよ（ﾎﾞｿｯ