Help us understand the problem. What is going on with this article?

NodeCGで配信を華やかに

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

NodeCGで何ができる?

NodeCGを使うとOBS組み込みブラウザ(ブラウザソース)に用意したHTMLを表示させて、それを外部から操作することができます。
文字じゃよくわからないと思いますが使ってみるとおおすげぇってなりますたぶん
自分の場合は以前ブラウザに表示させてそれをキャプチャして表示させていたんですが、書き換えている間は違うシーンに変えなきゃいけないし、誤ってウィンドウ閉じたりすると余計なものが映り込んだりしてしまっていたので、その方法もまた別に良いやり方があるのかもしれないが書き換えが必要なものをブラウザソースで表示させることができるのは凄い便利でした。

導入方法

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

実際につかってみる

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

package.json
{
  "name": "example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "MIT",

  "nodecg": {
    "compatibleRange": "^1.0.0"
  }
}

を置きます(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 com = document.getElementById("comment");
    const testRep = nodecg.Replicant("test");
    submitButton.onclick = () => {
        testRep.value = com.value;
    }
</script>
</html>

次にgraphicsフォルダの中に

index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
</head>
<body>
  <div id="simple" style="font-size: 100px; color: white"></div>
</body>

<script>
  const simple = document.getElementById("simple");
  const testRep = nodecg.Replicant("test");
  testRep.on("change", newValue => {
    simple.innerText = newValue;
  });
</script>
</html>

を作成します。

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

package.json
{
  "name": "example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "MIT",

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

これで完成です。動かしてみましょう。

コマンドプロンプトを開いてNodeCGのディレクトリ(bundlesフォルダのが入っているディレクトリ)に移動して実行します。

cd ここにNodeCGのディレクトリのアドレス
node .

しばらくすると

NodeCG running on http://localhost:9090

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

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

試合経過表示

HTMLやCSS部分はあんまりわかってないので参考にせずに、どんな感じに動かすかを見ていただければ…
※ここから先コードが長いので折りたたんでおきました。

まずはdashboardフォルダの中に

panel.htmlを作りました。
panel.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
  <style>
    * {
    font-family: "コーポレート・ロゴ(ラウンド)";
        font-size: 20px;
  }
    .team, .member {
        width: 370px;
    }
    .content {
        margin-bottom: 10px
    }
  </style>
</head>
<body>
    <div id="folder">
    <div class="content">
            <label>チームA</label>
            <input id="team1" class="team" type="text" placeholder="チーム名" onchange="teamSet()">
            <textarea id="member1" class="member" placeholder="メンバー"></textarea>
    </div>
    <div class="content">
            <label>チームB</label>
            <input id="team2" class="team" type="text" placeholder="チーム名" onchange="teamSet()">
          <textarea id="member2" class="member" placeholder="メンバー"></textarea>
    </div>
  </div>
    <label>ルール&ステージ</label>
    <div id="folder">
    <div class="content">
            <select id="result1">
                <option selected>-</option>
          </select><br>
            <select id="rule1">
            <option selected>? ? ?</option>
            <option>ナワバリ</option>
            <option>ガチエリア</option>
            <option>ガチヤグラ</option>
            <option>ガチホコ</option>
            <option>ガチアサリ</option>
          </select>
            <select id="stage1">
                <option value="none" selected>? ? ?</option>
                <option value="0">バッテラストリート</option>
                <option value="1">フジツボスポーツクラブ</option>
                <option value="2">ガンガゼ野外音楽堂</option>
                <option value="3">チョウザメ造船</option>
                <option value="4">海女美術大学</option>
                <option value="5">コンブトラック</option>
                <option value="6">マンタマリア号</option>
                <option value="7">ホッケふ頭</option>
                <option value="8">タチウオパーキング</option>
                <option value="9">エンガワ河川敷</option>
                <option value="10">モズク農園</option>
                <option value="11">Bバスパーク</option>
                <option value="12">デボン海洋博物館</option>
                <option value="13">ザトウマーケット</option>
                <option value="14">ハコフグ倉庫</option>
                <option value="15">アロワナモール</option>
                <option value="16">モンガラキャンプ場</option>
                <option value="17">ショッツル鉱山</option>
                <option value="18">アジフライスタジアム</option>
                <option value="19">ホテルニューオートロ</option>
                <option value="20">スメーシーワールド</option>
                <option value="21">アンチョビットゲームズ</option>
                <option value="22">ムツゴ楼</option>
                <option value="x">DEAR SENPAI</option>
            </select>
    </div>
        <div class="content">
            <select id="result2">
                <option selected>-</option>
          </select><br>
            <select id="rule2">
            <option selected>? ? ?</option>
            <option>ナワバリ</option>
            <option>ガチエリア</option>
            <option>ガチヤグラ</option>
            <option>ガチホコ</option>
            <option>ガチアサリ</option>
          </select>
            <select id="stage2">
                <option value="none" selected>? ? ?</option>
                <option value="0">バッテラストリート</option>
                <option value="1">フジツボスポーツクラブ</option>
                <option value="2">ガンガゼ野外音楽堂</option>
                <option value="3">チョウザメ造船</option>
                <option value="4">海女美術大学</option>
                <option value="5">コンブトラック</option>
                <option value="6">マンタマリア号</option>
                <option value="7">ホッケふ頭</option>
                <option value="8">タチウオパーキング</option>
                <option value="9">エンガワ河川敷</option>
                <option value="10">モズク農園</option>
                <option value="11">Bバスパーク</option>
                <option value="12">デボン海洋博物館</option>
                <option value="13">ザトウマーケット</option>
                <option value="14">ハコフグ倉庫</option>
                <option value="15">アロワナモール</option>
                <option value="16">モンガラキャンプ場</option>
                <option value="17">ショッツル鉱山</option>
                <option value="18">アジフライスタジアム</option>
                <option value="19">ホテルニューオートロ</option>
                <option value="20">スメーシーワールド</option>
                <option value="21">アンチョビットゲームズ</option>
                <option value="22">ムツゴ楼</option>
                <option value="x">DEAR SENPAI</option>
            </select>
    </div>
        <div class="content">
            <select id="result3">
                <option selected>-</option>
          </select><br>
            <select id="rule3">
            <option selected>? ? ?</option>
            <option>ナワバリ</option>
            <option>ガチエリア</option>
            <option>ガチヤグラ</option>
            <option>ガチホコ</option>
            <option>ガチアサリ</option>
          </select>
            <select id="stage3">
                <option value="none" selected>? ? ?</option>
                <option value="0">バッテラストリート</option>
                <option value="1">フジツボスポーツクラブ</option>
                <option value="2">ガンガゼ野外音楽堂</option>
                <option value="3">チョウザメ造船</option>
                <option value="4">海女美術大学</option>
                <option value="5">コンブトラック</option>
                <option value="6">マンタマリア号</option>
                <option value="7">ホッケふ頭</option>
                <option value="8">タチウオパーキング</option>
                <option value="9">エンガワ河川敷</option>
                <option value="10">モズク農園</option>
                <option value="11">Bバスパーク</option>
                <option value="12">デボン海洋博物館</option>
                <option value="13">ザトウマーケット</option>
                <option value="14">ハコフグ倉庫</option>
                <option value="15">アロワナモール</option>
                <option value="16">モンガラキャンプ場</option>
                <option value="17">ショッツル鉱山</option>
                <option value="18">アジフライスタジアム</option>
                <option value="19">ホテルニューオートロ</option>
                <option value="20">スメーシーワールド</option>
                <option value="21">アンチョビットゲームズ</option>
                <option value="22">ムツゴ楼</option>
                <option value="x">DEAR SENPAI</option>
            </select>
    </div>
        <div class="content">
            <select id="result4">
                <option selected>-</option>
          </select><br>
            <select id="rule4">
            <option selected>? ? ?</option>
            <option>ナワバリ</option>
            <option>ガチエリア</option>
            <option>ガチヤグラ</option>
            <option>ガチホコ</option>
            <option>ガチアサリ</option>
          </select>
            <select id="stage4">
                <option value="none" selected>? ? ?</option>
                <option value="0">バッテラストリート</option>
                <option value="1">フジツボスポーツクラブ</option>
                <option value="2">ガンガゼ野外音楽堂</option>
                <option value="3">チョウザメ造船</option>
                <option value="4">海女美術大学</option>
                <option value="5">コンブトラック</option>
                <option value="6">マンタマリア号</option>
                <option value="7">ホッケふ頭</option>
                <option value="8">タチウオパーキング</option>
                <option value="9">エンガワ河川敷</option>
                <option value="10">モズク農園</option>
                <option value="11">Bバスパーク</option>
                <option value="12">デボン海洋博物館</option>
                <option value="13">ザトウマーケット</option>
                <option value="14">ハコフグ倉庫</option>
                <option value="15">アロワナモール</option>
                <option value="16">モンガラキャンプ場</option>
                <option value="17">ショッツル鉱山</option>
                <option value="18">アジフライスタジアム</option>
                <option value="19">ホテルニューオートロ</option>
                <option value="20">スメーシーワールド</option>
                <option value="21">アンチョビットゲームズ</option>
                <option value="22">ムツゴ楼</option>
                <option value="x">DEAR SENPAI</option>
            </select>
    </div>
        <div class="content">
            <select id="result5">
                <option selected>-</option>
          </select><br>
            <select id="rule5">
            <option selected>? ? ?</option>
            <option>ナワバリ</option>
            <option>ガチエリア</option>
            <option>ガチヤグラ</option>
            <option>ガチホコ</option>
            <option>ガチアサリ</option>
          </select>
            <select id="stage5">
                <option value="none" selected>? ? ?</option>
                <option value="0">バッテラストリート</option>
                <option value="1">フジツボスポーツクラブ</option>
                <option value="2">ガンガゼ野外音楽堂</option>
                <option value="3">チョウザメ造船</option>
                <option value="4">海女美術大学</option>
                <option value="5">コンブトラック</option>
                <option value="6">マンタマリア号</option>
                <option value="7">ホッケふ頭</option>
                <option value="8">タチウオパーキング</option>
                <option value="9">エンガワ河川敷</option>
                <option value="10">モズク農園</option>
                <option value="11">Bバスパーク</option>
                <option value="12">デボン海洋博物館</option>
                <option value="13">ザトウマーケット</option>
                <option value="14">ハコフグ倉庫</option>
                <option value="15">アロワナモール</option>
                <option value="16">モンガラキャンプ場</option>
                <option value="17">ショッツル鉱山</option>
                <option value="18">アジフライスタジアム</option>
                <option value="19">ホテルニューオートロ</option>
                <option value="20">スメーシーワールド</option>
                <option value="21">アンチョビットゲームズ</option>
                <option value="22">ムツゴ楼</option>
                <option value="x">DEAR SENPAI</option>
            </select>
    </div>
  </div>
    <button id="submitButton">反映</button>
</body>
<script>
    const team1 = document.getElementById("team1");
    const team2 = document.getElementById("team2");
    const member1 = document.getElementById("member1");
    const member2 = document.getElementById("member2");
    const result1 = document.getElementById("result1");
    const result2 = document.getElementById("result2");
    const result3 = document.getElementById("result3");
    const result4 = document.getElementById("result4");
    const result5 = document.getElementById("result5");
    const rule1 = document.getElementById("rule1");
    const rule2 = document.getElementById("rule2");
    const rule3 = document.getElementById("rule3");
    const rule4 = document.getElementById("rule4");
    const rule5 = document.getElementById("rule5");
    const stage1 = document.getElementById("stage1");
    const stage2 = document.getElementById("stage2");
    const stage3 = document.getElementById("stage3");
    const stage4 = document.getElementById("stage4");
    const stage5 = document.getElementById("stage5");
    const submitButton = document.getElementById("submitButton");

    function teamSet() {
        let teams = [document.getElementById("team1").value, document.getElementById("team2").value];
        teams.forEach((team, index) => {
            for (var i = 1; i <= 5; i++) {
                let team_select = document.getElementById("result" + i)
                team_select.options[index+1] = new Option(team, team);
            }
        });
    }

    const dataRep = nodecg.Replicant("data", {persistent: false});

    submitButton.onclick = () => {
        let winArray = [result1.value,result2.value,result3.value,result4.value,result5.value];
        let winA = 0;
        let winB = 0;
        for (var i = 0; i < winArray.length; i++) {
            if (winArray[i] === team1.value) winA++;
            if (winArray[i] === team2.value) winB++;
        }
        dataRep.value = {
            "counter1": winA,
            "counter2": winB,
            "team1": team1.value,
            "team2": team2.value,
            "member1": member1.value,
            "member2": member2.value,
            "result1": result1.value,
            "result2": result2.value,
            "result3": result3.value,
            "result4": result4.value,
            "result5": result5.value,
            "rule1": rule1.value,
            "rule2": rule2.value,
            "rule3": rule3.value,
            "rule4": rule4.value,
            "rule5": rule5.value,
            "stage1": stage1.value,
            "stage2": stage2.value,
            "stage3": stage3.value,
            "stage4": stage4.value,
            "stage5": stage5.value
        };
    }
</script>
</html>

こんな感じで複数の値を送ることもできちゃいます。

次にgraphicsフォルダの中に

3games.htmlを作りました。
3games.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
  * {
    font-family: "コーポレート・ロゴ(ラウンド)";
  }
  p {
    text-align: center;
    font-family: "Splatoon2";
  }
  .counter {
    text-align: center;
    font-family: "Splatoon2";
    font-size: 120px;
  }
  .team {
    text-align: center;
    font-size: 60px;
  }
  .member {
    overflow: hidden;
    height: 3.0em;
    line-height: 1.5;
    text-align: center;
    font-size: 40px;
  }
  .result, .rule, .stage {
    font-size: 40px;
  }
  #teaminfo {
    width: 100%;
    margin: 3% 0 0;
  }
  #teamprof {
    width: 50%;
  }
  #folder {
    width: 80%;
    margin: 30px auto;
    display: -webkit-box;
    -webkit-box-pack: justify;
  }
  .content {
    width: 31%;
    height: auto;
    margin: 0 0 0 0;
  }
  .stage {
    width: 100%;
    height: auto;
  }
  .image {
    position: relative;
  }
  .image p {
    position: absolute;
    top: 0;
    left: 0;
    margin: 0;
    color: white;
    background: rgba(0, 0, 0, 0.6);;
    font-size: 30px;
    line-height: 1;
    padding: 5px 20px;
  }
  </style>
</head>
<body bgcolor="2C2F33" text="#FFFFFF" link="#00AEEF" vlink="#7289DA" alink="#FC0000">
  <center>
    <div id="teaminfo" style="display:inline-flex">
      <div id="teamprof">
        <div id="counter1" class="counter">0</div>
        <div id="team1" class="team">チーム名</div>
        <div id="member1" class="member">メンバー</div>
      </div>
      <p style="font-size: 100px">VS</p>
      <div id="teamprof">
        <div id="counter2" class="counter">0</div>
        <div id="team2" class="team">チーム名</div>
        <div id="member2" class="member">メンバー</div>
      </div>
    </div>
  </center>
  <div id="folder">
    <div class="content">
      <div id="result1" class="result">-</div>
      <div class="image">
        <img id="image1" src="./image/stage/none.png" class="stage">
        <p>1</p>
      </div>
      <div id="rule1" class="result">? ? ?</div>
      <div id="stage1" class="result">? ? ?</div>
    </div>
    <div class="content">
      <div id="result2" class="result">-</div>
      <div class="image">
        <img id="image2" src="./image/stage/none.png" class="stage">
        <p>2</p>
      </div>
      <div id="rule2" class="result">? ? ?</div>
      <div id="stage2" class="result">? ? ?</div>
    </div>
    <div class="content">
      <div id="result3" class="result">-</div>
      <div class="image">
        <img id="image3" src="./image/stage/none.png" class="stage">
        <p>3</p>
      </div>
      <div id="rule3" class="result">? ? ?</div>
      <div id="stage3" class="result">? ? ?</div>
    </div>
  </div>
</body>

<script>
  const counter1 = document.getElementById("counter1");
  const counter2 = document.getElementById("counter2");
  const team1 = document.getElementById("team1");
  const team2 = document.getElementById("team2");
  const member1 = document.getElementById("member1");
  const member2 = document.getElementById("member2");
  const result1 = document.getElementById("result1");
  const result2 = document.getElementById("result2");
  const result3 = document.getElementById("result3");
  const image1 = document.getElementById("image1");
  const image2 = document.getElementById("image2");
  const image3 = document.getElementById("image3");
  const rule1 = document.getElementById("rule1");
  const rule2 = document.getElementById("rule2");
  const rule3 = document.getElementById("rule3");
  const stage1 = document.getElementById("stage1");
  const stage2 = document.getElementById("stage2");
  const stage3 = document.getElementById("stage3");

  const dataRep = nodecg.Replicant("data", {persistent: false});

  const stageList = {"none":"? ? ?","0":"バッテラストリート","1":"フジツボスポーツクラブ","2":"ガンガゼ野外音楽堂","3":"チョウザメ造船","4":"海女美術大学","5":"コンブトラック","6":"マンタマリア号","7":"ホッケふ頭","8":"タチウオパーキング","9":"エンガワ河川敷","10":"モズク農園","11":"Bバスパーク","12":"デボン海洋博物館","13":"ザトウマーケット","14":"ハコフグ倉庫","15":"アロワナモール","16":"モンガラキャンプ場","17":"ショッツル鉱山","18":"アジフライスタジアム","19":"ホテルニューオートロ","20":"スメーシーワールド","21":"アンチョビットゲームズ","22":"ムツゴ楼","x":"DEAR SENPAI"};

  dataRep.on("change", newValue => {
    counter1.innerText = newValue.counter1;
    counter2.innerText = newValue.counter2;
    team1.innerText = newValue.team1;
    team2.innerText = newValue.team2;
    member1.innerText = newValue.member1;
    member2.innerText = newValue.member2;
    result1.innerText = newValue.result1;
    result2.innerText = newValue.result2;
    result3.innerText = newValue.result3;
    image1.src = "./image/stage/" + newValue.stage1 + ".png";
    image2.src = "./image/stage/" + newValue.stage2 + ".png";
    image3.src = "./image/stage/" + newValue.stage3 + ".png";
    rule1.innerText = newValue.rule1;
    rule2.innerText = newValue.rule2;
    rule3.innerText = newValue.rule3;
    stage1.innerText = stageList[newValue.stage1];
    stage2.innerText = stageList[newValue.stage2];
    stage3.innerText = stageList[newValue.stage3];
  });
</script>
</html>

みたいに作っていきます。
初期状態が映っても良いように適当な値を既に入れています。
また、persistent: falseにすると前回分のデータが残らなくなるみたいです。
画像とかはまあ適当に用意してください。
上でも述べましたが、綺麗なコードではないので動かし方だけ参考にしていただければ…

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

package.json
{
  "name": "example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "MIT",

  "nodecg": {
    "compatibleRange": "^1.0.0",
    "dashboardPanels": [
      {
        "width": "3",
        "name": "panel",
        "title": "試合経過",
        "file": "panel.html"
      },
      {
        "width": "1",
        "name": "test",
        "title": "コメント",
        "file": "test.html"
      }
    ],
    "graphics": [
      {
        "file": "3games.html",
        "width": 1920,
        "height": 1080
      },
      {
        "file": "5games.html",
        "width": 1920,
        "height": 1080
      },
      {
        "file": "index.html",
        "width": 800,
        "height": 600
      }
    ]
  }
}

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

またURLをコピーしてOBSにブラウザソースとして追加するんですが、今回は背景も含めたいのでカスタムCSSの部分を空白にします。サイズも数値を変更しておいてください。
image.png

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

感想とか

自分は作っていませんが、チーム名と勝数カウンターの部分を使って試合中に何対何なのかを表示できるようにしても面白そうです。あとは表示する部分にアニメーションとかも付けてみたいですね。上手くやれば書き換え自体も演出にできそう(?)。
間違っている部分とかあれば教えて下さい。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away