48
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

NodeCGで配信を華やかに

Last updated at Posted at 2019-10-24

Splatoon2の大会で運営としてOBSを使って運営放送(配信)をする際に放送をかっこよくするために何か良いものはないかと探しているうちにNodeCGというものを見つけました。

NodeCGで何ができる?

OBS組み込みブラウザ(ブラウザソース)に用意したHTMLを表示させて、それを外部から操作することができます
文字じゃよくわからないと思いますが使ってみるとおおすげぇってなりますたぶん

自分の場合は以前ブラウザに表示させてそれをキャプチャして表示させていたんですが、書き換えている間は違うシーンに変えなきゃいけないし、誤ってウィンドウ閉じてしまったり余計なものが映り込んだりしてしまう可能性があるので、その方法もまた別に良いやり方があるのかもしれないが書き換えが必要なものをブラウザソースで表示させることができるのはとても便利でした。

導入方法

についてはcma2819氏Hoishin氏が詳しく説明してくださっているのでそちらを参考にしてみてください。

実際につかってみる

NodeCGのbundlesフォルダの中にフォルダを作ります。

nodecg
└─bundles
    └─example

以後ここで作ったフォルダ名をexampleとして進めていきます。
その中に

package.json
{
  "name": "example",
  "version": "1.0.0",
  "nodecg": {
    "compatibleRange": "^1.0.0"
  }
}

を置きます(nameはフォルダ名と同じにしておいてください)。
NodeCGを動かす際に必要となります。
加えて、exampleフォルダの中にdashboardとgraphicsというフォルダを作っておいてください。

今こんな感じ
現在の構成

コメント表示

まずはdashboardフォルダの中に

test.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
</head>

<body>
  <input id="comment" type="text">
  <button id="submitButton">反映</button>
</body>
<script>
  const comment = document.getElementById("comment");
  const submitButton = document.getElementById("submitButton");

  const testRep = nodecg.Replicant("test");

  submitButton.addEventListener("click", () => {
    testRep.value = comment.value;
  });
</script>

</html>

次にgraphicsフォルダの中に

index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
</head>

<body>
  <div id="simple" style="font-size: 150px; color: white;"></div>
</body>

<script>
  const simple = document.getElementById("simple");

  const testRep = nodecg.Replicant("test", { defaultValue: "" });

  testRep.on("change", (newValue) => {
    simple.innerText = newValue;
  });
</script>

</html>

を作成します。

できたら先程exampleフォルダに置いたpackage.jsonに書き加えます。

package.json
{
  "name": "example",
  "version": "1.0.0",
  "nodecg": {
    "compatibleRange": "^1.0.0",
    "dashboardPanels": [
      {
        "width": 2,
        "name": "test",
        "title": "コメント",
        "file": "test.html"
      }
    ],
    "graphics": [
      {
        "file": "index.html",
        "width": 800,
        "height": 600
      }
    ]
  }
}

これでひとまず完成です。
image.png
動かしてみましょう。

ターミナルなどを開いてNodeCGのディレクトリに移動して実行します。

cd ここにnodecgディレクトリのアドレス
npm start

しばらくすると

info: [nodecg/lib/server] Starting NodeCG [NodeCGのバージョン] (Running on Node.js v[Node.jsのバージョン])
info: [nodecg/lib/server] NodeCG running on http://localhost:9090

みたいに表示されるので表示されたURL(上の場合はhttp://localhost:9090)をブラウザで開きます。
すると
dashboard
何かかっこいいのが表示されました。これがダッシュボードで、ここで操作します。
このままだと表示するものがないので右上のGRAPHICSと書いてある目のマークをクリックしてください。
すると
graphics
こんな感じの画面が出てくるのでCOPY URLをクリックし、OBSを起動してコピーされたURLのブラウザソースを追加してください。
image.png
追加した状態だと値が入っていないので何も表示されていません。
ひとまず先程開いたダッシュボードのWORKSPACEをクリックして操作画面に戻ります。
コメントと書いてある所に適当な文字を入力して反映ボタンをクリックしてみてください。
test.gif
変わった!!!!!
と、こんな感じで動かせます。

もっといろいろやってみましょう。

試合経過表示

※ここから先コードが長いので一部折りたたんであります。
こんなのも作れるんだくらいにみてもらえれば幸いです。

まずはdashboardフォルダの中に

panel.htmlを作りました。
panel.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
</head>

<body>
  <div class="teams">
    <div class="team">
      <label>チームA</label>
      <input class="name" type="text" placeholder="チーム名">
      <textarea class="member" placeholder="メンバー"></textarea>
    </div>
    <div class="team">
      <label>チームB</label>
      <input class="name" type="text" placeholder="チーム名">
      <textarea class="member" placeholder="メンバー"></textarea>
    </div>
  </div>
  <label>試合結果</label>
  <div class="games">
    <div class="game">
      <select class="winner"></select>
      <select class="rule"></select>
      <select class="stage"></select>
    </div>
    <div class="game">
      <select class="winner"></select>
      <select class="rule"></select>
      <select class="stage"></select>
    </div>
    <div class="game">
      <select class="winner"></select>
      <select class="rule"></select>
      <select class="stage"></select>
    </div>
    <div class="game">
      <select class="winner"></select>
      <select class="rule"></select>
      <select class="stage"></select>
    </div>
    <div class="game">
      <select class="winner"></select>
      <select class="rule"></select>
      <select class="stage"></select>
    </div>
  </div>
  <button id="submitButton">反映</button>
</body>

<script>
  const rules = [
    "ナワバリ", "ガチエリア", "ガチヤグラ", "ガチホコ", "ガチアサリ"
  ];
  const stages = [
    "バッテラストリート",
    "フジツボスポーツクラブ",
    "ガンガゼ野外音楽堂",
    "チョウザメ造船",
    "海女美術大学",
    "コンブトラック",
    "マンタマリア号",
    "ホッケふ頭",
    "タチウオパーキング",
    "エンガワ河川敷",
    "モズク農園",
    "Bバスパーク",
    "デボン海洋博物館",
    "ザトウマーケット",
    "ハコフグ倉庫",
    "アロワナモール",
    "モンガラキャンプ場",
    "ショッツル鉱山",
    "アジフライスタジアム",
    "ホテルニューオートロ",
    "スメーシーワールド",
    "アンチョビットゲームズ",
    "ムツゴ楼"
  ];

  const random = "? ? ?";
  const blank = "-";

  const nameInputs = document.getElementsByClassName("name");
  const memberInputs = document.getElementsByClassName("member");
  const winnerInputs = document.getElementsByClassName("winner");
  const ruleInputs = document.getElementsByClassName("rule");
  const stageInputs = document.getElementsByClassName("stage");
  
  const teamSet = () => {
    Array.from(nameInputs).forEach((name, index) => {
      Array.from(winnerInputs).forEach(input => {
        input.options[index+1] = new Option(name.value);
      });
    });
  };

  Array.from(nameInputs).forEach(input => {
    input.addEventListener("change", teamSet);
  });
  Array.from(winnerInputs).forEach(input => {
    input.options[0] = new Option(blank);
  });
  Array.from(ruleInputs).forEach(input => {
    const ruleOptions = rules.map(rule => new Option(rule));
    [new Option(random), ...ruleOptions].forEach((option, index) => {
      input.options[index] = option;
    });
  });
  Array.from(stageInputs).forEach(input => {
    const stageOptions = stages.map(stage => new Option(stage));
    [new Option(random), ...stageOptions].forEach((option, index) => {
      input.options[index] = option;
    });
  });

  const submitButton = document.getElementById("submitButton");

  const resultRep = nodecg.Replicant("result");

  submitButton.addEventListener("click", () => {
    resultRep.value = {
      teams: Array.from(nameInputs).map((name, index) => {
        return {
          name: name.value,
          member: memberInputs[index].value
        };
      }),
      games: Array.from(winnerInputs).map((winner, index) => {
        return {
          winner: winner.value,
          rule: ruleInputs[index].value,
          stage: stageInputs[index].value
        };
      })
    };
  });
</script>

<style>
  * {
    font-size: 15px;
    font-family: 'FOT-くろかね Std', sans-serif;
  }

  .teams, .games {
    display: flex;
    flex-flow: column;
    gap: 10px;
    margin-bottom: 30px;
  }

  .team {
    display: flex;
    flex-flow: column;
  }

  .winner {
    display: block;
  }

  .rule, .stage {
    display: inline-block;
  }
</style>

</html>

こんな感じでオブジェクトにすることで複数の値を送ることもできちゃいます。

次にgraphicsフォルダの中に

bo3.htmlを作りました。
bo3.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
</head>

<body>
  <div class="container">
    <div class="teams">
      <div class="team">
        <div class="counter">0</div>
        <div class="name">チーム名</div>
        <div class="member">メンバー</div>
      </div>
      <div class="vs">VS</div>
      <div class="team">
        <div class="counter">0</div>
        <div class="name">チーム名</div>
        <div class="member">メンバー</div>
      </div>
    </div>
    <div class="games">
      <div class="game">
        <div class="winner">-</div>
        <div class="display">
          <img class="image" src="./image/stage/random.png">
          <div class="number">1</div>
        </div>
        <div class="rule">? ? ?</div>
        <div class="stage">? ? ?</div>
      </div>
      <div class="game">
        <div class="winner">-</div>
        <div class="display">
          <img class="image" src="./image/stage/random.png">
          <div class="number">2</div>
        </div>
        <div class="rule">? ? ?</div>
        <div class="stage">? ? ?</div>
      </div>
      <div class="game">
        <div class="winner">-</div>
        <div class="display">
          <img class="image" src="./image/stage/random.png">
          <div class="number">3</div>
        </div>
        <div class="rule">? ? ?</div>
        <div class="stage">? ? ?</div>
      </div>
    </div>
  </div>
</body>

<script>
  const bo = 3;
  const random = "? ? ?";

  const counterSpaces = document.getElementsByClassName("counter");
  const nameSpaces = document.getElementsByClassName("name");
  const memberSpaces = document.getElementsByClassName("member");
  const winnerSpaces = document.getElementsByClassName("winner");
  const ruleSpaces = document.getElementsByClassName("rule");
  const stageSpaces = document.getElementsByClassName("stage");
  const imageSpaces = document.getElementsByClassName("image");

  const resultRep = nodecg.Replicant("result", { persistent: false });

  resultRep.on("change", newValue => {
    if (!newValue) return;
    newValue.teams.forEach((team, index) => {
      nameSpaces[index].innerHTML = team.name;
      memberSpaces[index].innerText = team.member;
      const win = newValue.games.filter((game) => game.winner === team.name && index < bo);
      counterSpaces[index].innerHTML = win.length;
    });
    newValue.games.forEach((game, index) => {
      if (index < bo) {
        winnerSpaces[index].innerHTML = game.winner;
        ruleSpaces[index].innerHTML = game.rule;
        stageSpaces[index].innerHTML = game.stage;
        imageSpaces[index].src = `./image/stage/${game.stage === random ? "random" : game.stage}.png`;
      }
    });
  });
</script>

<style>
  body {
    margin: 0;
  }

  .container {
    background-color: #2c2f33;
    color: #fff;
    font-size: 10vh;
    position: relative;
    width: 100vw;
    height: 100vh;
    overflow: hidden;
    font-family: 'FOT-くろかね Std', sans-serif;
  }

  .teams {
    position: absolute;
    top: 6vh;
    width: 80vw;
    margin: 0 10vw;
    display: flex;
    justify-content: space-between;
  }

  .vs {
    margin: auto 0;
  }

  .team {
    width: 31%;
    text-align: center;
  }

  .counter {
    font-size: 120%;
    line-height: 1.5;
  }

  .name {
    font-size: 50%;
  }

  .member {
    font-size: 35%;
    line-height: 1.5;
  }

  .games {
    position: absolute;
    bottom: 10vh;
    width: 80vw;
    margin: 0 10vw;
    display: flex;
    justify-content: space-between;
    font-size: 35%;
  }

  .game {
    width: 31%;
    overflow: hidden;
  }

  .display {
    position: relative;
    width: 100%;
    margin-bottom: -5%;
  }

  .image {
    width: 100%;
    object-fit: contain;
  }

  .number {
    position: absolute;
    top: 0;
    left: 0;
    width: 12%;
    background: rgba(0, 0, 0, 0.6);
    color: white;
    text-align: center;
    font-size: 70%;
  }

  .rule, .stage {
    line-height: 1.5;
  }
  
</style>

</html>

こんな感じで作っていきます。

今回はpersistent: falseにすることで前回分のデータが残らないようにして、初期値をHTML上に既に入れています。
画像とかはまあ適当に用意してください。

先程と同様にpackage.jsonに書き加えていきます。
ここでは書きませんがbo3.htmlと同じようにbo5.htmlも作りました。

package.json
{
  "name": "example",
  "version": "1.0.0",
  "nodecg": {
    "compatibleRange": "^1.0.0",
    "dashboardPanels": [
      {
        "width": "3",
        "name": "panel",
        "title": "試合経過",
        "file": "panel.html"
      },
      {
        "width": 2,
        "name": "test",
        "title": "コメント",
        "file": "test.html"
      }
    ],
    "graphics": [
      {
        "file": "bo3.html",
        "width": 1920,
        "height": 1080
      },
      {
        "file": "bo5.html",
        "width": 1920,
        "height": 1080
      },
      {
        "file": "index.html",
        "width": 800,
        "height": 600
      }
    ]
  }
}

最終的なファイル構成
image.png

これでもう一度実行します。
すると
image.png
image.png
項目が増えました。

URLをコピーしてOBSにブラウザソースとして追加するんですが、幅と高さの数値を自分が作ったものと同じ値に変更しておいてください。
また、bodyを背景として使う場合はカスタムCSSの部分を空白にしておいてください。
image.png

入力欄に入力して反映ボタンをクリックすれば画面内でも変わります。
test2.gif
同じように作ったbo5.htmlも同じ操作パネルで同時に変更できます。

感想とか

最近はReactSolid.jsなんかと合わせて使ってます。
表示の切り替えにアニメーションを付けたりできると切り替え自体も演出にできるかも

48
42
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
48
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?