NLE(Niconico Live Encoder)はどうやって配信用URLを取得しているのか？


NLEではセッションを消費せずに配信用のURLやキーを取得しています。この動きと同じことが実装できれば、配信ツールでセッションを一個消費ということも無くなるはずです。ということで、解析結果とサンプルコード(Ruby)になります。

## 全体的な動作

1. アップデート確認
2. ログイン
3. ライセンス取得
4. ログイン
5. 配信情報確認
6. 動画配信！

なお、アップデート確認とライセンス取得は不要です。

## ログイン

### Request

* URL: https://account.nicovideo.jp/api/v1/login
* Method: POST
* User Agent: nicoliveenc/{NLEバージョン番号}
* Content Type: application/x-www-form-urlencoded
* Form Data:
	* site: nicolive_encoder
	* time: {UNIXタイム}
	* hash_key: {8桁16進数} # 省略可
	* mail: {メールアドレス}
	* password: {パスワード}

hash_keyの生成方法は不明です。なくても認証できます。

### Response

* Set Cookie:
	* Name: nicosid
	* Vlaue: {UNIXタイム}.{数字}
	* Path: /
	* Domain: .nicovideo.jp
* Data: XML

	```xml
	<nicovideo_user_response status="ok">
		<ticket>nicolive_encoder_{数字}</ticket>
	</nicovideo_user_response>
	```

必要なのはticketのコンテンツのみです。nicosidは別に使わなくてもいいようです。もし、ログインに失敗している場合はstatusがfailになります。

## バージョン確認

URL: http://live.nicovideo.jp/encoder/update.xml

NLEの最新バージョンを確認します。必須ではありません。詳細は省略。

## ライセンス取得

URL: http://live.nicovideo.jp/encoder/getlicence

NLEのライセンスを取得します。必須ではありません。詳細は省略。

## 配信情報取得

### Request

* URL: http://live.nicovideo.jp/api/getpublishstatus
* Method: POST
* User Agent: nicoliveenc/{NLEバージョン番号}
* Content Type: application/x-www-form-urlencoded
* Cookie: nicosid={loginで取得した値} # 省略可
* Form Data:
	* ticket: {loginで取得したticketの値}
	* nleserial: {getlicenceで取得したシリアル番号} # 省略可
	* accept-multi: {0/1}

Cookieは渡さなくてもいいようです。nlserialにはライセンス取得で取得したシリアル番号が必要ですが、なくても問題ありません。accept-multiは一つのみ取得(0)か複数取得(1)を指定できます。値によって結果のXMLが変わります。

### Response

accept-multi=0の場合

* Data: XML

	```xml
	<getpublishstatus status="ok" time="1432436641" multi="true">
		<stream>{動画の情報 省略}</stream>
		<user>{ユーザーの情報 省略}</user>
		<rtmp is_fms="1">
			<url>rtmp://{ホスト}.live.nicovideo.jp:{ポート番号}/publicorigin/{数字}</url>
			<stream>lv{数字}</stream>
			<ticket>{ユーザID}:{ライブID}:{数字}:{数字}:{数字}:{数字}:{16進数}</ticket>
			<bitrate>{数字}</bitrate>
		</rtmp>
	</getpublishstatus>
	```

accept-multi=1の場合

* Data: XML

	```xml
	<getpublishstatus status="ok" time="1432436641" multi="true">
		<list>
			<item>
				<stream>{動画の情報 省略}</stream>
				<rtmp is_fms="1">
					<url>rtmp://{ホスト}.live.nicovideo.jp:{ポート番号}/publicorigin/{数字}</url>
					<stream>lv{数字}</stream>
					<ticket>{ユーザID}:{ライブID}:{数字}:{数字}:{数字}:{数字}:{16進数}</ticket>
					<bitrate>{数字}</bitrate>
				</rtmp>
			</item>
		</list>
		<user>{ユーザーの情報 省略}</user>
	</getpublishstatus>
	```

statusがokなら配信可能です。配信できないときはfailが入ります。accept-multi=0の場合は、単純にアクセスしたときと同じです。accept-multi=1の場合は、配信の情報だけ、list化されてitemに入れられます。実際の配信URLとストリームキーは下記になります。

* 配信URL: {urlの値}?{tickeの値}
* ストリームキー: {streamの値}

bitrateは現在配信可能なビットレートを表します。

## サンプルコード

以上を踏まえて、配信URL、ストリームキー、ビットレートを取得するツールを作りました。煮るなり、焼くなり、Go言語製コメビュに移植するなり、ご自由に！

```simnle.rb
# coding: utf-8

require "net/https"
require "uri"
require "nokogiri"

class SimNLE

  NICOURI = {
    login: URI("https://account.nicovideo.jp/api/v1/login"),
    getpub: URI("http://live.nicovideo.jp/api/getpublishstatus"),
  }

  USER_AGENT = "simnle.rb/0.0.1"

  def initialize(mail, password)
    @mail = mail
    @password = password
    @ticket = nil
    @nicosid = nil
  end

  def post_header
    header = {
      "User-Agent" => SimNLE::USER_AGENT,
      "Content-Type" => "application/x-www-form-urlencoded",
    }
    if @nicosid
      header["Cookie"] = "nicosid=#{URI.encode_www_form_component(@nicosid)}"
    end
    return header
  end

  def login
    https = Net::HTTP.new(SimNLE::NICOURI[:login].host, 443)
    https.use_ssl = true
    https.verify_mode = OpenSSL::SSL::VERIFY_NONE

    data = {
      "site" => "nicolive_encoder",
      "time" => Time.now.to_i.to_s,
      "mail" => @mail,
      "password" => @password,
    }

    response = nil
    https.start do
      response = https.post(SimNLE::NICOURI[:login].path,
          URI.encode_www_form(data), post_header)
    end
    response.value

    doc = Nokogiri::XML.parse(response.body)
    case doc.xpath("/nicovideo_user_response").attribute("status").to_s
    when "ok"
      @ticket = doc.xpath("/nicovideo_user_response/ticket").inner_text
      response.get_fields("set-cookie").each do |cookie_data|
        list = cookie_data.split(";")[0].strip.split("=")
        if list[0] == "nicosid"
          @nicosid = list[1]
        end
      end
      return @ticket
    when "fail"
      return nil
    else
      raise "Unknown status"
    end
  end

  def getpub
    unless @ticket || login
      raise "Cannot login"
    end

    http = Net::HTTP.new(SimNLE::NICOURI[:getpub].host, 80)

    data = {
      "ticket" => @ticket,
      "accept-multi" => 0,
    }

    response = nil
    http.start do
      response = http.post(SimNLE::NICOURI[:getpub].path,
          URI.encode_www_form(data), post_header)
    end
    response.value

    doc = Nokogiri::XML.parse(response.body)
    case doc.xpath("/getpublishstatus").attribute("status").to_s
    when "ok"
      url = doc.xpath("/getpublishstatus/rtmp/url").inner_text
      stream = doc.xpath("/getpublishstatus/rtmp/stream").inner_text
      ticket = doc.xpath("/getpublishstatus/rtmp/ticket").inner_text
      bitrate = doc.xpath("/getpublishstatus/rtmp/bitrate").inner_text
      rtmp_url = "#{url}?#{ticket}"
      rtmp_key = stream
      return rtmp_url, rtmp_key, bitrate
    when "fail"
      return nil
    else
      raise "Unknown status"
    end
  end
end

if __FILE__ == $0
  if ARGV.size < 2
    warn "Usage: ruby #{$0} mail password"
    exit 1
  end
  simnle = SimNLE.new(ARGV[0], ARGV[1])
  # puts simnle.login
  puts simnle.getpub
end
```