LoginSignup
5
2

NodeCGで配信画面を作ってみよう(Reactを使おう編)

Last updated at Posted at 2023-12-14

はじめに

この記事はただカッコいいという理由だけでNodeCGを使った配信レイアウトを作ってみた備忘録の第2弾です。

今回は前回作成した現在時刻をオーバーレイ表示する機能、テロップを表示する機能をReactを使って実装していきます。

前回の記事はこちらから。

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

プロジェクトのセットアップ

さて、早速Bundleプロジェクトの設定をしていきましょう。
まずは以下のようなBundleディレクトリを作成します。(Bundle名はtemp-bundleとします)

temp-bundle
┣ src
┃ ┣ dashboard
┃ ┣ extension
┃ ┗ graphics
┣ tsconfig.json
┗ package.json

TypeScriptの設定

せっかくなのでTypeScriptを使ってBundleを作成してみましょう。

まずは必要なパッケージをインストールします。

npm install -D ts-nodecg

ts-nodecgはNodeCGのメンテナーであるHoshin氏の作成したパッケージで、Bundle特有のReplicantやMessageの型定義を盛り込んだNodeCGの型定義を利用することができます。

tsconfig.json

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"]
}

message.d.ts

Bundleで用いるMessageのプロパティとやり取りするデータ型の定義を行います。

src/message.d.ts
export type MessageMap = {
	"updateTelop": {
		data: string
	}
}

replicant.ts

Bundleで用いるReplicantの型定義、および初期値を設定します。

src/replicant.ts
export type ReplicantMap = {
    "telop": string
}

// Replicantの初期値を定義
export const replicantDefaultValues: ReplicantMap = {
    "telop": "sample-telop"
}

nodecg.d.ts

Bundleで用いるReplicant,Messageの型定義をNodeCGの型定義に追加します。

src/nodecg.d.ts
import type { CreateNodecgInstance } from 'ts-nodecg/browser';
import type { ReplicantMap } from './replicant';
import { MessageMap } from './message';

export type NodecgInstance = CreateNodecgInstance<
    "temp-bundle",
    undefined,
    ReplicantMap,
    MessageMap
>;

global.d.ts

ブラウザ側で使用するグローバル変数、nodecgを宣言します。

src/global.d.ts
import type { NodecgInstance } from "./nodecg";

declare global {
  const nodecg: NodecgInstance;
}

Reactの設定

さて、次は今回の記事のメインテーマであるReactの設定です。

まずは必要なパッケージをインストールします。
今回はお手軽にそれっぽいダッシュボードのレイアウトを作りたいのでMUIを追加しています。

npm install react react-dom @emotion/react @emotion/styled @mdi/react @mui/material
npm install -D @types/react @types/react-dom

Replicant用カスタムフック作成

ReactでReplicantを扱うためのカスタムフックを作成します。
replicant.tsで作成したReplicantの型定義を用いたTypeScript対応版です。
こちらの記事を大変参考にさせていただきました。)

src/hooks.ts
import { useCallback, useEffect, useState } from 'react';
import { replicantDefaultValues, ReplicantMap } from './replicant';

export const useReplicant = <T extends keyof ReplicantMap>(
  name: T
): [ReplicantMap[T] | undefined, (newValue: ReplicantMap[T]) => void] => {
  const [rep] = useState(() =>
    nodecg.Replicant(name, {
      defaultValue: replicantDefaultValues[name]
    })
  );
  const [value, setValue] = useState(rep.value);
  useEffect(() => {
    const handleChange = (newValue: ReplicantMap[T]) => setValue(newValue);
    rep.on('change', handleChange);
    return () => {
      rep.removeListener('change', handleChange);
    };
  }, [rep]);
  return [value, useCallback(newValue => (rep.value = newValue), [rep])];
};

Reactテンプレート

Reactのテンプレートとして以下のファイルを使用します。
viteのReact×TypeScriptテンプレートから諸々を抜いて単純化した内容です。

react-template
┣ index.html
┣ index.css
┣ main.tsx
┗ App.tsx

graphics/telop

テロップ表示レイアウトの設定です。
useReplicantフックを用いてReplicantの値を受け取って表示しています。

src/graphics/telop/App.tsx
import { useReplicant } from "../../hooks";

function App() {
    const [ telop ] = useReplicant<"telop">("telop");
    return (
        <div id="telop">{telop}</div>
    )
}
export default App
cssは前回と同じ
src/graphics/telop/index.css
body {
    background-color: transparent;
    width: 1920px;
    height: 1080px;
    margin: 0;
    display: flex;
    align-items: flex-end;
}

#root {
    width: 100%;
}

#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;
}

dashboard/telop

テロップ操作用ダッシュボードのソースです。
前回作ったダッシュボードパネルをそのままReactで作り直しただけなので特筆して書くことはないですね。

src/dashboard/telop/App.tsx
import { Button, Card, CardActions, CardContent, TextField } from "@mui/material";
import { useState } from "react";

function App() {
    const [ telop, setTelop ] = useState("");
    const updateTelop = () => {
        nodecg.sendMessage("updateTelop", telop);
    }
    return (
        <Card>
            <CardContent>
                <TextField multiline value={telop} onChange={(e) => setTelop(e.target.value)} fullWidth />
            </CardContent>
            <CardActions>
                <Button variant="contained" onClick={updateTelop}>更新</Button>
            </CardActions>
        </Card>
    )
}
export default App

graphics/clock

現在時刻をオーバーレイ表示するレイアウトの設定です。
こちらも特筆して書くことはなし。

src/graphics/clock
src/graphics/clock/App.tsx
import { useState } from "react"

function App() {
    const [ time, setTime ] = useState(new Date().toLocaleTimeString());
    setInterval(() => {
        setTime(new Date().toLocaleTimeString())
    }, 1000);
    return (
        <div id="time">{time}</div>
    )
}
export default App
src/graphics/clock/.css
body {
    background-color: transparent;
}
#time {
    font-size: 120px;
    color: #ffffff;
    width: fit-content;
    text-shadow: 0 0 10px orange, 0 0 15px orange;
}

extensionの設定

extensionはサーバーサイドの処理なのでReactは使わずに普通にTypeScriptで実装していきます。

src/extension/index.ts
import { replicantDefaultValues } from '../replicant';
import { NodecgInstance } from '../nodecg';

export default (nodecg: NodecgInstance) => {

    // テロップReplicantの初期化
    const telopRep = nodecg.Replicant("telop", {
        defaultValue: replicantDefaultValues['telop']
    })

    const updateTelop = (message: string) => {
        telopRep.value = message;
    }

    nodecg.listenFor("updateTelop", updateTelop);
}

型定義を利用している以外は特筆すべき箇所もなく、ほぼ前回と同じです。
Replicantの宣言時に型定義の恩恵が受けられますね。

ビルドの設定

今回はviteを使ってビルドを行います。
src配下のextension, graphics, dashboardそれぞれをビルドしてBundle配下に出力するようviteの設定を行います。

↓こんな感じで出力したい

temp-bundle
┣ extension
┃ ┗ index.js 
┣ dashboard
┃ ┣ assets
┃ ┗ telop
┃   ┣ index.js
┃   ┗ index.html
┗ graphics
  ┣ assets
  ┣ telop
  ┃ ┣ index.js
  ┃ ┗ index.html
  ┗ clock
    ┣ index.js
    ┗ index.html

viteの設定

例によってまずはパッケージのインストールからです。

npm install -D vite @vitejs/plugin-react

extension

extensionのビルド設定を行います。

src/extension/vite.config.ts
import { defineConfig } from 'vite'
import { resolve } from 'path'

const root = resolve(__dirname);

const outDir = resolve(__dirname, "../../extension");

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [],
    root: root,
    build: {
        outDir: outDir,
        lib: {
            entry: resolve(root, "index.ts"),
            name: "extension"
        },
        rollupOptions: {
            output: {
                dir: outDir,
                entryFileNames: `index.js`
            }
        }
    }
})

extensionはindex.jsが関数をエクスポートする必要があるのでlibオプションを使ってライブラリとしてエクスポートしています。

graphics

src/graphics/vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'
import { readdirSync } from 'fs';

const root = resolve(__dirname);

const outDir = resolve(__dirname, "../../graphics");

const dirList = readdirSync(root, {
    withFileTypes: true, 
}).filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);

const input = (() => {
    const retVal: {[key: string]: string} = {}
    for (const dir of dirList) {
        retVal[dir] = resolve(root, dir, "index.html");
    }
    return retVal;
})();

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [react()],
    root: root,
    base: "./",
    build: {
        outDir: outDir,
        manifest: true,
        rollupOptions: {
            input: input,
            output: {
                dir: outDir,
                entryFileNames: `[name]/index.js`
            }
        }
    }
})

dashboard

dashboardとgraphicsはほとんど同じなので閉じておきます。

src/dashboard/vite.config.ts
src/dashboard/vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'
import { readdirSync } from 'fs';

const root = resolve(__dirname);

const outDir = resolve(__dirname, "../../dashboard");

const dirList = readdirSync(root, {
    withFileTypes: true, 
}).filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);

const input = (() => {
    const retVal: {[key: string]: string} = {}
    for (const dir of dirList) {
        retVal[dir] = resolve(root, dir, "index.html");
    }
    return retVal;
})();


// https://vitejs.dev/config/
export default defineConfig({
    plugins: [react()],
    root: root,
    base: "./",
    build: {
        outDir: outDir,
        manifest: true,
        rollupOptions: {
            input: input,
            output: {
                dir: outDir,
                entryFileNames: `[name]/index.js`
            }
        }
    }
})

dashboard、graphicsは各パネルやレイアウト毎にそれぞれ分けて出力する必要があるのでrollupOptionsの設定を弄ってgraphics・dashboard配下のフォルダ毎に出力されるようにしています。

これで後々dashboardやgraphicsを増やす際にもbuild設定を変更する必要がないようになったはずです。(全て同じフォーマットのReactを使う前提なのでReact以外を使いたくなったら要修正)

package.jsonの修正

script, nodecgの設定を追加します。

package.json
{
  "name": "temp-bundle",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "build": "cd src/dashboard && vite build && cd ../graphics && vite build && cd ../extension && vite build"
  },
  "nodecg": {
    "compatibleRange": "*",
    "dashboardPanels": [
      {
        "name": "telop",
        "title": "テロップ",
        "width": 5,
        "height": 10,
        "file": "telop/index.html",
        "workspace": "workspace"
      }
    ],
    "graphics": [
      {
        "file": "clock/index.html",
        "width": "1920",
        "height": "1080"
      },
      {
        "file": "telop/index.html",
        "width": "1920",
        "height": "1080"
      }
    ]
  },
  // 以下略
}

アクセスしてみよう

temp-bundleをビルドしてからNodeCGを起動してみましょう。

cd bundles/temp-bundle

npm run build

cd ../..

npm start

Videotogif (2) (1).gif

前回と同様の配信レイアウトをReactを使って作成できました!

おわりに

さて、今回はReactを使おう編ということで色々やってきましたが、TypeScriptやviteでのビルドなど、だいぶ設定内容が多くてゴチャゴチャしてしまいました。

何はともあれ、無事にReactを使ったNodeCGのBundle作成ができましたね。
素のHTML、JavaScriptを書くよりは断然慣れているのでこれできっと開発もやりやすくなることでしょう。

また気が向いたらNodeCGの記事を書こうと思うので期待せずにお待ちいただけると幸いです。

参考

5
2
1

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
5
2