前書き的なもの
PX-S1UD-1を手に入れたのでRaspberryPi4を有効活用してみようと視聴環境を構築したものの、物足りなさにニコニコ実況を実装しようと思いその事前調査した記録。
何番煎じだろうかw
出先でアニメ視聴用に地デジチューナーの低遅延HLS配信を多重Proxyと端末認証で垂れ流していた際はリソース余りまくりのラックサーバーでページの動画領域取得して無理矢理クロマキーしたりしていたが、スマートじゃないなと思いコメントのSocket取得を簡単にできるようにしてみようと思ったのも動機の一つ。
コメントの取得に関わる通信
以下のページとその参照元、引用元を覗いてみるとWebSocketで動画情報、コメントを配信しているらしい。
ニコ生新配信の放送をアプリで再生するための覚書き - Qiita
ニコ生のコメント送受信をWebSocket+JSONでやる方法ざっくり解説 - Qiita
これらを大雑把にまとめると以下の流れでコメント取得できそう。
Get Comment
動画ページ ==> Socket用アドレス取得
↓
コンテンツ用Socketのセッション確立
↓
コンテンツ用Socketでコンテンツ取得要求 ==> 動画再生の為の情報が送られてくる
↓
コメント用Socketのアドレスと必要な情報の抜き出し
↓
コメント用Socketのセッション確立
↓
コメント用Socketにコンテンツ用Socketで発行されたIDを用いてコメント取得要求
↓
コメントが送られてくる
これをNodejs辺りで行えば簡単にコメント取得できそう。
Nodejsでコメント取得
Chromeブラウザのデベロッパツールとjavascriptで実際にコメント取得の手順を説明している記事を発見。
ニコニコ生放送のコメントを取得して色々するための第一歩(前編:JavaScript版) - Qiita
超分かり易かったのでこれを参考にさせてもらってNodejsで組んでみた。
使ったパッケージは「websocket」と「puppeteer-core」にLinux環境はブラウザとして「chromium」
Get Comment
const puppeteer = require('puppeteer-core')
let dir_Brwsr = '';
let url_page = (process.argv[2] || 'https://live.nicovideo.jp/watch/ch2646485');
let channel_name = "";
let socket_view = '';
const message_system_1 = '{"type":"startWatching","data":{"stream":{"quality":"abr","protocol":"hls","latency":"low","chasePlay":false},"room":{"protocol":"webSocket","commentable":true},"reconnect":false}}';
const message_system_2 ='{"type":"getAkashic","data":{"chasePlay":false}}'
let uri_comment
let threadID
//Browser Directory (WinはEdge、LinuxはChromiumの判別)
if(process.platform==='win32') dir_Brwsr = 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe';
else if(process.platform==='darwin') dir_Brwsr = '';
else if(process.platform==='linux') dir_Brwsr = '/usr/bin/chromium';
//Browser Controle
async function getLatestDate(page, url){
await page.goto(url) // Open URL Page
// Browser JavaScript
channel_name = await page.evaluate(() => JSON.parse(document.getElementById("embedded-data").getAttribute("data-props")).socialGroup.name);
return await page.evaluate(() => JSON.parse(document.getElementById("embedded-data").getAttribute("data-props")).site.relive.webSocketUrl); //ヘッドレスブラウザで開いてsocketアドレス取得
}
!(async() => {
try {
const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox'],executablePath: (dir_Brwsr),ignoreDefaultArgs: ['--disable-extensions']});
const page = await browser.newPage();
const url_view = await getLatestDate(page, url_page);
console.log(channel_name);
console.log("WebSocket Connection ==> " + url_view);
client.connect(url_view, null, null, {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'}, null);
browser.close()
} catch(e) {
console.error(e)
}
})()
//View Session: WebSocket Connection
let WebSocketClient = require('websocket').client;
let client = new WebSocketClient();
client.on('connectFailed', function(error) {
console.log('View Session Connect Error: ' + error.toString());
});
client.on('connect', function(connection) {
console.log('WebSocket Client Connected[View Session]');
socket_view = connection; //コメントSocketから閉じられるようにコネクション格納
connection.sendUTF(message_system_1); //コンテンツ情報要求
connection.sendUTF(message_system_2);
connection.on('error', function(error) {
console.log("View Session Connection Error: " + error.toString());
});
connection.on('close', function() {
console.log('WebSocket Client Closed[View Session]');
});
connection.on('message', function(message) {
if (message.type === 'utf8') {
// Get Comment WWS Addres & Option Data
if(message.utf8Data.indexOf("room")>0) { //色々データ抜いてコメントsocketに接続
evt_data_json = JSON.parse(message.utf8Data);
uri_comment = evt_data_json.data.messageServer.uri
threadID = evt_data_json.data.threadId
message_comment = '[{"ping":{"content":"rs:0"}},{"ping":{"content":"ps:0"}},{"thread":{"thread":"'+threadID+'","version":"20061206","user_id":"guest","res_from":-150,"with_global":1,"scores":1,"nicoru":0}},{"ping":{"content":"pf:0"}},{"ping":{"content":"rf:0"}}]'
console.log("WebSocket Connection ==> " + uri_comment);
// Comment WebSocket Connection
comclient.connect(uri_comment, 'niconama', {
headers: {
'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits',
'Sec-WebSocket-Protocol': 'msg.nicovideo.jp#json',
},
});
}
// Keep View Session
if(message.utf8Data.indexOf("ping")>0) { //pingに応答
connection.sendUTF('{"type":"pong"}');
connection.sendUTF('{"type":"keepSeat"}');
}
}
});
});
// Comment Session: WebSocket Connection
let comclient = new WebSocketClient();
comclient.on('connectFailed', function(comerror) {
console.log('Comment Session Connect Error: ' + comerror.toString());
});
comclient.on('connect', function(connection) {
console.log('WebSocket Client Connected[Comment Session]');
connection.sendUTF(message_comment);
// Comment Session Keep Alive
setInterval((connection)=>{connection.sendUTF("");}, 60000, connection); //コメントSocketの生存確認送信
connection.on('error', function(error) {
console.log("Comment Session Connection Error: " + error.toString());
});
connection.on('close', function() {
console.log('WebSocket Client Closed[Comment Session]');
socket_view.close(); //コメントSocket終了時、コンテンツsocketも終了
});
connection.on('message', function(message) {
if (message.type === 'utf8') {
if (message.utf8Data.indexOf("chat")>0){ //コメント以外スルー
let baff = JSON.parse(message.utf8Data);
if (baff.chat.content.indexOf('spi')<=0 && baff.chat.content.indexOf('nicoad')<=0){ //広告コメントスルー
//console.log('Received:' + message.utf8Data); //コメントのjson(コメントの色、位置などのコマンドあり)をコンソール出力
console.log('Received Coment: ' + baff.chat.content); //コメント文字のみをコンソール出力
}
}
}
});
});
ポイントとしては他の記事でも書いてある通りコンテンツ用Socket開通時に適当なブラウザのユーザーエージェントをヘッダーに付与してブラウザからのアクセスを装う事と、コンテンツ用Socketで「Ping」に応答しつつ「keepSeat」も一緒に送信して動画を視聴しているような状態にしてセッションを確立し続ける事。
さらにコメントセッション確立後はコメント送信しない場合、約1分おきにコメントSocketでメッセージを送信して通信が生きていることを示す事。
ちなみにコメント用Socketはコンテンツ用SocketとIDで紐づけられており、コンテンツSocketが閉じるとコメントSocketも閉じるようになっている。
逆に放送が終了していないのにコメント用Socketが何らかの影響で閉じてしまってもコンテンツ用Socketは閉じずに生きているので、上記のプログラムではコンテンツ用Socketのコネクションを変数に格納してコメント用Socketが終了した際にコンテンツ用Socketも終了するようにしている。
一応、使い方説明
使い方は「nodejs」インストール後、今回使用している必要なパッケージもnpmインストールして実行。
今回WebページからSocketアドレスを取得するのに「puppeteer-core」を使用しているが、これはchrome系のブラウザを使用するので「Chrome」か「Chromium」もインストールする。
Windows環境では標準インストールの「Edge」を使用するのでブラウザインストール不要。
Mac環境はChrome系ブラウザインストール後、実行ファイルのパスをプログラムに要記述。
nodejs & Browser install
$ curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
$ apt update
$ sudo apt install -y nodejs Chromium
$ sudo npm install -g websocket puppeteer-core
適当な名前のjsファイルで保存してnodejsで実行。
実行時にニコニコ実況、生放送のURLを指定するとそこからコメントをリアルタイムに取得する。
URLの指定がない場合は初期値として設定されているニコニコ実況の「TOKYO MX」のコメントをリアルタイム取得する。
node nico_comment Run
$ nodejs nico_comment.js https://live.nicovideo.jp/watch/ch2646485
後書き的なもの
今回のプログラムではとりあえずコンソールにコメントを出力するようにしているので必要な方は個々人で取得したコメントの処理を書いてください。
あと、nodejs単体で実行した場合はニコニコ実況だと朝4時に一度放送が終了して切り替わる際にSocketも閉じてプログラムも終了します。
うちの場合はサーバーのDockerコンテナでReStart="Always"設定で運用しているのでプログラム終了してもコンテナがRestartして再接続するので再接続処理書いてません。
↧