はじめに
この記事はただカッコいいという理由だけで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
{
"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のプロパティとやり取りするデータ型の定義を行います。
export type MessageMap = {
"updateTelop": {
data: string
}
}
replicant.ts
Bundleで用いるReplicantの型定義、および初期値を設定します。
export type ReplicantMap = {
"telop": string
}
// Replicantの初期値を定義
export const replicantDefaultValues: ReplicantMap = {
"telop": "sample-telop"
}
nodecg.d.ts
Bundleで用いるReplicant,Messageの型定義をNodeCGの型定義に追加します。
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を宣言します。
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対応版です。
(こちらの記事を大変参考にさせていただきました。)
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の値を受け取って表示しています。
import { useReplicant } from "../../hooks";
function App() {
const [ telop ] = useReplicant<"telop">("telop");
return (
<div id="telop">{telop}</div>
)
}
export default App
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で作り直しただけなので特筆して書くことはないですね。
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
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
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で実装していきます。
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のビルド設定を行います。
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
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
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の設定を追加します。
{
"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
前回と同様の配信レイアウトをReactを使って作成できました!
おわりに
さて、今回はReactを使おう編ということで色々やってきましたが、TypeScriptやviteでのビルドなど、だいぶ設定内容が多くてゴチャゴチャしてしまいました。
何はともあれ、無事にReactを使ったNodeCGのBundle作成ができましたね。
素のHTML、JavaScriptを書くよりは断然慣れているのでこれできっと開発もやりやすくなることでしょう。
また気が向いたらNodeCGの記事を書こうと思うので期待せずにお待ちいただけると幸いです。
参考