【SHOWROOM】配信中のコメント・ギフトログをリアルタイムに取得する方法

# はじめに
最近SHOWROOMを見始めて、バーチャル枠を見てると配信画面にコメントやらギフトログを出している人が居るのでそれってどうやって取得できるのかな？と思ったのがこの記事を書くことになった切っ掛けです

# 調査
双方向通信って事はWebSocketでも使っているのかな？
と言う事で配信画面でブラウザの開発者ツール開いて`WS`を見てみると案の定
`online.showroom-live.com`に`SUB 何かの文字列`を送ることで配信内のコメントなど取得できるようになってるみたいです
![ws.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310163/51dbe726-8812-efc1-a1a4-725f430e25ea.png)

### 謎の文字列
WebSocket使っているのは分かったが、接続した後何の文字列を送っているのか？
色々なルームの文字列を見たところ`bXXXXX(6文字):XXXXXXXX(8文字)`の形式みたいです

色々調べてみるとAPIがあるみたいで[まとめてあるページ](https://qiita.com/takeru7584/items/f4ba4c31551204279ed2)があったので参考に調べてみたところ
https://www.showroom-live.com/api/live/onlives
にアクセスした際各ルームの`bcsvr_key`と言う所の値がそれっぽいみたいです

bcsvrが何なのか調べてみたらDeNAが開発したギフティングやコメント向けのリアルタイムサーバみたいです

# 接続してみる
ささっとJavaScriptでコンソールに出力して中身を見てみます

```javascript
// WebSocket 接続を作成
const socket = new WebSocket('wss://online.showroom-live.com')

// 接続が開いたときのイベント
socket.onopen = (e) => {
    socket.send('SUB\tbXXXXX:XXXXXXXX')
}

// メッセージの待ち受け
socket.onmessage = (message) => {
    console.log(message.data)
}
```

コンソールに出力される内容を見てみると、基本的に下記のような形式で取得されているみたいです

```
// コメント
MSG	bXXXXX:XXXXXXXX	{"ua":値,"av":値,"d":値,"ac":"値","cm":"値","created_at":値,"u":値,"at":値,"t":"値"}
// ギフト
MSG	bXXXXX:XXXXXXXX	{"ua":値,"n":値,"av":値,"d":値,"ac":"値","created_at":値,"u":値,"h":値,"g":値,"gt":値,"at":値,"t":"値"}
// テロップ
MSG	bXXXXX:XXXXXXXX	{"telops":[{"color":{"r":値,"b":値,"g":値},"text":"値","type":"値"},{"color":{"r":値,"b":値,"g":値},"text":"値","type":"値"}],"telop":"値","interval":値,"t":値,"api":"値"}
// 疎通確認
ACK showroom
// 不明
MSG bXXXXX:XXXXXXXX {"created_at":値,"t":値}
MSG bXXXXX:XXXXXXXX {"created_at":値,"u":値,"at":値,"t":値}
// エラー（海外配信者のルームでよく見かける）
Could not decode a text frame as UTF-8.
```
疎通確認時とエラーのテキスト以外、最初の`MSG bXXXXX:XXXXXXXX `を除いた後の文字列はJSONだと思われるのでJSONに変換したら色々加工できると推測

JavaScriptでJSONオブジェクトとして使えるように加工

```javascript
// （略）
// メッセージの待ち受け
socket.onmessage = (message) => {
    if (message.data === "ACK\tshowroom" || message.data === "Could not decode a text frame as UTF-8.") {
        console.log("疎通確認又はエラー")
    } else {
        // JSONオブジェクトとして取得
        console.log(JSON.parse(message.data.replace("MSG\tbXXXXX:XXXXXXXX", "")))
    }
}
```

# コメントログとギフトログを取得
コンソールのログを見てみるとコメントログの要素数が9、ギフトログの要素数が12と言う事が分かっています
なのでJSONに変換した後、要素数で分けてあげればコメントログとギフトログに分ける事ができます

```javascript
// （略）
// メッセージの待ち受け
socket.onmessage = (message) => {
    if (message.data === "ACK\tshowroom" || message.data === "Could not decode a text frame as UTF-8.") {
        console.log("疎通確認はエラー")
    } else {
        // JSONオブジェクトとして取得
        let messageJson = JSON.parse(message.data.replace("MSG\tbXXXXX:XXXXXXXX", ""))
        if (Object.keys(messageJson).length === 9) {
            // コメントログ
        } else if (Object.keys(messageJson).length === 12) {
            // ギフトログ
        } else {
            // テロップなど（今回は無視）
        }
    }
}
```

# JSON要素の構成
### コメント

```
{
    ac: "ユーザ名",
    at: 0（不明、固定っぽい？）,
    av: アバターID,
    cm: "コメント",
    created_at: 投稿時間,
    d: 0（不明、固定っぽい？）,
    t: "1"（不明、固定っぽい？1がコメント？）,
    u: ユーザID,
    ua: リピーター(3)、初見(2)、ビギナー(1),
}
```

### ギフト

```
{
    ac: "ユーザ名",
    at: 0（不明、固定っぽい？）,
    av: アバターID,
    created_at: 投稿時間,
    d: 0（不明、固定っぽい？）,
    g: ギフトID,
    gt: 無料(1)、有料(2),
    h: 0（不明）,
    n: ギフトの個数,
    t: "2"（不明、固定っぽい？2がギフト？）,
    u: ユーザID,
    ua: リピーター(3)、初見(2)、ビギナー(1),
}
```

# 画面にログを出してみる
画面を準備

```html
<div>
    <div id="comment">
        <!-- コメントログ -->
    </div>
    <div id="gift">
        <!-- ギフトログ -->
    </div>
</div>
```

JavaScriptも少し修正しておきます

```javascript
// （略）
// メッセージの待ち受け
socket.onmessage = (message) => {
    if (message.data === "ACK\tshowroom" || message.data === "Could not decode a text frame as UTF-8.") {
        console.log("疎通確認又はエラー")
    } else {
        // JSONオブジェクトとして取得
        let messageJson = JSON.parse(message.data.replace("MSG\tbXXXXX:XXXXXXXX", ""))
        if (Object.keys(messageJson).length === 9) {
            // コメントログ
            showComment(messageJson) // 追加
        } else if (Object.keys(messageJson).length === 12) {
            // ギフトログ
            showGift(messageJson) // 追加
        } else {
            // テロップなど（今回は無視）
        }
    }
}
```

### コメント
表示に必要な項目は

- ユーザ名
- アバターID
- コメント

の3つがあれば十分なのでコメントに関してはこの3項目を使用

```javascript
function showComment(comment) {
    // アバター画像要素
    let avaElement = document.createElement('img')
    avaElement.src = "https://image.showroom-cdn.com/showroom-prod/image/avatar/" + comment.av + ".png"
    avaElement.width = 40
    avaElement.height = 40
    // コメント要素
    let commentElement = document.createElement('p')
    commentElement.textContent = comment.ac + ":" + comment.cm
    // アバターとコメントを纏める
    let divElement = document.createElement('div')
    divElement.appendChild(avaElement)
    divElement.appendChild(commentElement)
    // 画面に表示
    document.getElementById('comment').appendChild(divElement)
}
```


### ギフト
表示に必要な項目は

- ユーザ名
- アバターID
- ギフトID
- ギフト個数

の4つがあれば十分なのでギフトに関してはこの4項目を使用

```javascript
function showGift(gift) {
    // アバター画像要素
    let avaElement = document.createElement('img')
    avaElement.src = "https://image.showroom-cdn.com/showroom-prod/image/avatar/" + gift.av + ".png"
    avaElement.width = 40
    avaElement.height = 40
    // ギフト画像要素
    let gitfElement = document.createElement('img')
    gitfElement.src = "https://image.showroom-cdn.com/showroom-prod/assets/img/gift/" + gift.g + "_s.png"
    gitfElement.width = 20
    gitfElement.height = 20
    // ギフト個数要素
    let commentElement = document.createElement('p')
    commentElement.textContent = gift.ac + ":" + gift.n + "個"
    // アバターとギフトを纏める
    let divElement = document.createElement('div')
    divElement.appendChild(avaElement)
    divElement.appendChild(gitfElement)
    divElement.appendChild(commentElement)
    // 画面に表示
    document.getElementById('gift').appendChild(divElement)
}
```

# 完成形

ソースの全体像は下記を参照してください

```html
<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <title>SRコメント、ギフト取得</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
    <div>
        <div id="comment"></div>
        <div id="gift"></div>
    </div>

    <script>
        window.onload = function () {
            // WebSocket 接続を作成
            const socket = new WebSocket('wss://online.showroom-live.com')

            // 接続が開いたときのイベント
            socket.onopen = (e) => {
                socket.send('SUB\tbXXXXX:XXXXXXXX')
            }

            // メッセージの待ち受け
            socket.onmessage = (message) => {
                if (message.data === "ACK\tshowroom" || message.data === "Could not decode a text frame as UTF-8.") {
                    console.log("疎通確認又はエラー")
                } else {
                    // JSONオブジェクトとして取得
                    let messageJson = JSON.parse(message.data.replace("MSG\tbXXXXX:XXXXXXXX", ""))
                    if (Object.keys(messageJson).length === 9) {
                        // コメントログ
                        showComment(messageJson)
                    } else if (Object.keys(messageJson).length === 12) {
                        // ギフトログ
                        showGift(messageJson)
                    } else {
                        // テロップなど（今回は無視）
                    }
                }
            }
        }

        function showComment(comment) {
            // アバター画像要素
            let avaElement = document.createElement('img')
            avaElement.src = "https://image.showroom-cdn.com/showroom-prod/image/avatar/" + comment.av + ".png"
            avaElement.width = 40
            avaElement.height = 40
            // コメント要素
            let commentElement = document.createElement('p')
            commentElement.textContent = comment.ac + ":" + comment.cm
            // アバターとコメントを纏める
            let divElement = document.createElement('div')
            divElement.appendChild(avaElement)
            divElement.appendChild(commentElement)
            // 画面に表示
            document.getElementById('comment').appendChild(divElement)
        }

        function showGift(gift) {
            // アバター画像要素
            let avaElement = document.createElement('img')
            avaElement.src = "https://image.showroom-cdn.com/showroom-prod/image/avatar/" + gift.av + ".png"
            avaElement.width = 40
            avaElement.height = 40
            // ギフト画像要素
            let gitfElement = document.createElement('img')
            gitfElement.src = "https://image.showroom-cdn.com/showroom-prod/assets/img/gift/" + gift.g + "_s.png"
            gitfElement.width = 20
            gitfElement.height = 20
            // ギフト個数要素
            let commentElement = document.createElement('p')
            commentElement.textContent = gift.ac + ":" + gift.n + "個"
            // アバターとギフトを纏める
            let divElement = document.createElement('div')
            divElement.appendChild(avaElement)
            divElement.appendChild(gitfElement)
            divElement.appendChild(commentElement)
            // 画面に表示
            document.getElementById('gift').appendChild(divElement)
        }
    </script>
</body>

</html>
```

# 最後に
後はお好みでCSSを適応させて見栄えよくさせればオリジナルのコメントビューワーを手軽に作成することができます！