Ruby
ニコニコ動画
3分ハッキング

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

More than 3 years have passed since last update.

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

    <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

    <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

    <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