NodeCGで配信画面を作ってみよう（NodeCGとは？編）

## はじめに

この記事はただカッコいいという理由だけで動的な配信画面を作ってみたくなった成人男性の備忘録です。

この記事はTDCソフト株式会社Advent Calendarの11日目です。

https://qiita.com/advent-calendar/2023/tdc

## NodeCGとは
NodeCGはNode.jsとブラウザを使ってライブ配信画面を作ることのできるフレームワークです。
OBS Studio等のストリーミングソフト単体では実装の難しい動的なライブ配信画面を実現できるという特徴があります。
近年RTA in Japanで使用されているということでも有名なフレームワークです。

### 実際どんなものが作れるの？

先に挙げたRTA in Japanの配信アーカイブです。
この配信画面もNodeCGを用いて作成されているそうです。

https://www.youtube.com/playlist?list=PLFvJYuQufMw4HsAf8hDzzjabtzLcZO9lG

## プロジェクトを作ってみよう

公式が提供しているNodeCGのプロジェクト作成方法としては以下の2点があります。
1. NodeCG CLIを使う
1. GitHubからプロジェクトをクローンする

今回はCLIを使ってプロジェクトを作成します。
任意のプロジェクトフォルダを作成し、以下のコマンドを打ちます。

```bash
# cliをインストール
npm install --global nodecg-cli@latest

# プロジェクトを作成
nodecg setup
```

## 動かしてみる

コマンドの実行が正常に完了すると以下のようなファイルが生成されます。
```
live-app
┣ build
┣ bundles
┣ db
┣ node_modules
┣ schemas
┣ scripts
┣ AUTHORS
┣ index.js
┣ LICENSE
┣ package-lock.json
┣ package.json
┗ README.md
```

それぞれのフォルダの細かい説明は後にして一旦動かしてみましょう。
```bash
# NodeCGを起動する
npm start
```

[http://localhost:9090](http://localhost:9090)へアクセスするとダッシュボード画面が表示されます。

![image01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3125818/ffe0b50e-4fe1-de4c-2988-c2fd1035936c.jpeg)


・・・が、このままだと何もできないので何かできるようにしていきましょう。

## 配信レイアウトを設定する

### Bundleとは？

NodeCGで配信レイアウトを作るにはBundleを設定する必要があります。
BundleとはGraphicsやDashboard、Extensionsといった機能をまとめたものです。

| 名前 | 用途 |
|--|--|
| Graphics | 配信に乗せるレイアウト<br>  |
| Dashboard | Graphicsの表示内容を変更・操作するためのダッシュボード |
| Extensions | GraphicsやDashboardのバックエンド部分<br> 初期化処理や外部との通信を行う |

・・・分かりにくいですね。
いい感じの配信レイアウトパッケージ、くらいの認識でいいと思います。


### Bundleをインストールする

このBundleは自分で用途に合ったものを自由に作成することもできますが、誰かが公開してくれている素敵なレイアウトを利用させてもらうことも可能です。

今回は利用しませんが、こちらもCLIを使ってGitHubからインストールすることができます（普通にgit cloneしてbundles/配下に置いてもOKです）。

```bash
# 以下の形式でGitHubからインストール可能
nodecg install username/repo-name
```


## Bundleを作ってみよう

さて、早速簡単なBundleを作ってみましょう。
まずはbundles/配下にプロジェクトを作成します。

```bash
cd bundles

mkdir sample-bundle

cd sample-bundles

npm init -y
```

### graphics

まずは配信レイアウト（Graphics）から作ってみましょう。
graphicsフォルダを作成し、htmlファイルを作ります。
単純に現在時刻をオーバーレイ表示するだけのレイアウトにしてみましょう。

```clock.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>Clock</title>
</head>
<style>
    body {
        background-color: transparent;
    }
    #time {
        font-size: 40px;
        color: #ffffff;
        width: fit-content;
        text-shadow: 0 0 10px orange, 0 0 15px orange;
    }
</style>
<body>
    <div id="time"></div>
</body>
<script type="text/javascript">
    document.getElementById("time").innerText = new Date().toLocaleTimeString();
    setInterval(() => {
        document.getElementById("time").innerText = new Date().toLocaleTimeString();
    }, 1000);
</script>
</html>
```

clock.htmlができたところでpackage.jsonにbundleの設定内容を記述します。

```sample-bundle/package.json
{
  "name": "sample-bundle",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  // ↓ここから
  "nodecg": {
    "compatibleRange": "*",
    "dashboardPanels": [],
    "graphics": [
      {
        "file": "clock.html",
        "width": "1920",
        "height": "1080"
      }
    ]
  },
  // ↑ここまで追加
  "author": "",
  "license": "ISC"
}
```

`"nodecg": {...}`部分がBundleの設定箇所、内部の`"graphics": [...]`がGraphicsの設定箇所です。

| 項目 | 説明 |
|--|--|
| file | 表示するHTMLファイル<br> graphicsフォルダからの相対パスで表示する。 |
| width | width |
| height | height |

:::note info
dashboardPanelsはDashboardに関する設定を記述する箇所です。
今回は不要ですが、必須項目なので記述しています。
:::

この状態でNodeCGを起動し、Graphicsタブを見に行くとbandleに設定したGraphics情報が反映されています。

![02.graphics.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3125818/7fd83d23-2139-32a0-5d82-e6563c56ebb5.jpeg)


CLOCK.HTMLのURLをコピーし、OBS Studioのブラウザソースから追加すると
配信画面に現在時刻をオーバーレイ表示することができます（ニュース番組風レイアウトとか作れそうですね）。
![2023-12-03_20-36-19.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3125818/7ea1ffe3-c93c-aa8e-db2e-f476b017d8b5.gif)


### Dashboard

さて、次は配信画面の設定を色々弄ったりするためのダッシュボード画面を作ります。
今回は配信レイアウトに表示するテロップを自由に書き換えられるようにしてみましょう。
graphicsと同様にdashboardフォルダを作成し、その中にtelop.htmlを作ります。

```sample-bundle/dashboard/telop.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <meta lang="ja" />
    <title>telop</title>
</head>
<body>
    <form onsubmit="return updateTelop();">
        <textarea type="text" id="telop"></textarea>
        <br>
        <input type="submit" value="更新" />
    </form>
</body>
<script type="text/javascript">

const telopReplicant = nodecg.Replicant("telop", {
    defaultValue: "sample telop"
});

function updateTelop() {
    const telopValue = document.getElementById("telop").value;
    telopReplicant.value = telopValue;
    return false;
}
</script>
</html>
```

#### Replicant
さて、ここで何やら見慣れないものが出てきましたね。
ReplicantはNodeCGの提供する機能の一つでExtensions-Graphics-Dashboard間で共通の値を扱うことができるオブジェクトです。
また、値はディスク上に保存されるため、NodeCGを再起動したとしても以前の値が自動的に復元されます。


```js
// telopという名前のReplicantオブジェクトを宣言している
const telopReplicant = nodecg.Replicant("telop", {
    defaultValue: "sample telop"
});
```

さて、テロップの値を操作するダッシュボードを作成したので、今度はテロップを表示するHTMLをGraphicsに追加します。

```sample-bundle/graphics/telop.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>Telop</title>
</head>
<style>
    body {
        background-color: transparent;
        width: 1920px;
        height: 1080px;
        margin: 0;
        display: flex;
        align-items: flex-end;
    }
    #telop {
        width: 100%;
        font-size: 36px;
        min-height: 128px;
        display: flex;
        align-items: center;
        color: #f0f0f0;
        background-color: rgba(16, 16, 16, 0.8);
        padding: 10px;
    }
</style>
<body>
    <div id="telop"></div>
</body>
<script type="text/javascript">

// Replicant "telop"の値が変更されたらテキストを変更している
nodecg.Replicant("telop").on("change", (newValue, oldValue) => {
    document.getElementById("telop").innerText = newValue;
});
</script>
</html>
```

ここでもReplicantを宣言していますね。
dashboard/telop.html内で宣言したからといってGraphicsでも自動的に使えるというわけではなく、Replicantは使用したい箇所でそれぞれ宣言しなければ使えません。
既に同じ名前のReplicantがBundle内で宣言されている場合は自動的に既存の値が割り当てられます。

ここまで来たら先程と同様、package.jsonに設定内容を追記します。

```sample-bundle/package.json
{
  "name": "sample-bundle",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "nodecg": {
    "compatibleRange": "*",
    "dashboardPanels": [
      // ↓ここから
      {
        "name": "telop",
        "title": "telop",
        "width": 5,
        "file": "telop.html",
        "workspace": "workspace"
      }
      // ↑ここまで
    ],
    "graphics": [
      {
        "file": "clock.html",
        "width": "1920",
        "height": "1080"
      },
      // ↓ここから
      {
        "file": "telop.html",
        "width": "1920",
        "height": "1080"
      }
      // ↑ここも
    ]
  },
  "author": "",
  "license": "ISC"
}

```

`"dashboardPanels": [...]`にDashboardの設定を追加します。

| 項目 | 説明 |
|--|--|
| name | ダッシュボードの識別子<br> 同じnameを複数設定するとエラーになる |
| title | ダッシュボードパネルのタイトル |
| width | ダッシュボードパネルの横幅<br> 1-10の間で指定する<br> (144 * width - 16)pxの幅になる |
| file | 表示するHTMLファイル<br> dashboardフォルダからの相対パスで表示する |
| workspace | パネルを表示するタブ<br>  |


さて、それではダッシュボードにアクセスしてみましょう。

![04.telop.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3125818/e957727d-042e-0ab9-0acf-2c96c1dc3dd8.jpeg)


無事、telop.htmlがWORKSPACEタブのパネルに追加されました。

clock.htmlの時と同様にOBS Studioにブラウザソースを追加したものがこちらです
（mp4ファイルを適当にgifに変換した為か時の進みが歪んでいるのは気にしないでください）。
![05.telop (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3125818/d5633765-57cf-7ecc-0be1-537f89169233.gif)


formを更新したタイミングで配信レイアウト側にもテロップの変更内容が反映されていますね。
このようにReplicantを使うことでDashboard-Graphics間で同じ値を扱うことができ、DashboardからGraphicsに表示する内容をコントロールできます。

### Extensions

さて、Bundle三種の神器（私が勝手にそう呼んでいる）の最後の一つ、Extensionsに触れていきましょう。
Extensionsは先に挙げた二つとは異なり、サーバーサイドで動作するという特徴があります。
また、ExtensionsはBundleの中で一番最初に実行されるコードなので、Bundleで使うReplicantの初期化やその他データのセットアップを行うのに適しています。
また、そのほかにも定期的なReplicantの更新、他のWebサイトからのデータ取得といった用途にも適しています。

Extensionsは以下の2つの方法で設定可能です。
1. extension.jsファイルを作る
1. extension/index.jsファイルを作る

:::note warn
どちらのファイルも存在している場合はNodeCG起動時にエラーが発生します。
以下の用途によって使い分けるのが推奨されています。
1. extensionsに記述する処理が少なく、1ファイルにすべての処理を記述する場合
1. 複数ファイルに処理を分割して記述する場合
:::

というわけでReplicantの初期化・更新処理をextensionsに任せてみましょう。

```sample-bundle/extension/index.js
module.exports = function (nodecg) {
    // テロップReplicantの初期化
    const telopReplicant = nodecg.Replicant("telop", {
        defaultValue: "sample telop"
    });

    // telopの更新処理を行う関数
    const updateTelop = (message) => {
        telopReplicant.value = message;
    }

    // messageを受け取ったらtelopReplicantの値を更新する
    nodecg.listenFor("telop", updateTelop);
}
```

初期化・更新処理をextensionsに移したのでDashBoardのpanel.htmlも修正します。

```sample-bundle/dashboard/panel.html
<script type="text/javascript">
function updateTelop() {
    const telopValue = document.getElementById("telop").value;

    // messageを送信してサーバーサイドで更新処理を行う
    nodecg.sendMessage("telop", telopValue);
    return false;
}
</script>
```

これでReplicantの初期化・更新処理をサーバーサイド（Extensions）のみに行わせることができます。
Dashboardからはmessageをサーバーサイドに飛ばしてExtensionsで更新処理を行わせています。
このような実装にすることでデータフローがすっきりし、処理がわかりやすくなるというメリットがあります（今回はあまりに単純な処理しかしていないのでさほど恩恵はありませんが・・・）。


## おわりに

さて、NodeCGとは何か？から簡単なBundleの実装までの流れを書いてみましたがいかがだったでしょうか。
最初はReplicantが何のことやら全然わからず苦戦しましたが、色々やりたいことができそうでとてもワクワクしております。

今回はNodeCGとは？編ということで基礎的なことを書きましたが、もう少し踏み込んだ内容の記事を書きたい気持ちもあるので期待せずに待っていてくだされば幸いです。


## 参考

https://www.nodecg.dev/ja/

https://blog.livewing.net/nodecg-react

https://zenn.dev/cma2819/articles/nodecg-implements-replicant

https://hackmd.io/@HiST/SkwhnBKjv