配信オーバーレイフレームワーク「NodeCG」のご紹介と使い方

1. NodeCG とは

NodeCG とは、ライブ配信につきものの「オーバーレイ」をレンダリングするためのオープンソース・ツールです。

配信を日常的に実施している方には釈迦に説法ですが、オーバーレイとは、配信画面の上部レイヤーに合成するテロップやネームカード、装飾等を指します。

vMix や OBS Studio 等の配信ソフトに、大抵はこれらのオーバーレイを表示する機能がありますが、 配信ソフト上で仕込みをしたり更新したりする必要があります。

NodeCG ではオーバーレイを HTML,CSS,JavaScript でコーディングすることで、配信ソフトよりも高い自由度でオーバーレイを作成でき、 配信ソフトでは不可能な、柔軟かつリアクティブなデータバインディングとワークフローを実現します。

プログラミング技術が必要で、仕込みの工数も多くなるので、 データ更新がなくワークフローが単純な配信の場合、わざわざ NodeCG を使用する必要はありません。

NodeCG を検討する上では、以下の特徴を抑えておく必要があります。

  • グラフィカルな編集ツールではない。画面作りこみにはコーディングが必要
  • 実体は Node.js でホストされる Web アプリケーションであり、配信ソフトとは別のプロセスとして起動され、 配信ソフトのブラウザーインプットを使用して配信画面に合成する
  • 配信中にテロップテキストやアセットを更新するには、予めコーディングによってダッシュボードの仕込みが必要
  • フルスタックフレームワークなのでサーバーサイドの処理が可能。例えば外部から NodeCG サーバーにデータを送信したり等

Web 開発者の方なら、Express や Next.js、Meteor.js 等でも同じことが出来ると感じるかもしれないですが、 普通の Web アプリケーションとして開発を始めると CMS 機能や DB とのリアクティブなデータバインディング、 セキュリティ等を自前で実装する必要があり、NodeCG を使えばその分の手間を省くことが可能です。

NodeCG は個人配信であまり必要がないかもしれませんが、 逐次更新されるデータを配信画面に表示したい、 あるいは複数のオペレーターが画面更新に関わるワークフローを実現したい場合に本領を発揮します。

本記事では、サンプルケースとして、筆者が趣味でやっているドライブ配信のオーバーレイを NodeCG で実装します。 ドライブ配信はソフトを操作できませんから、位置情報等のデータを配信画面に表示したければ、何らかの方法で自動化する必要があります。

NodeCG は配信オーバーレイだけでなく、「デジタルサイネージ」にも使えると思います。

NodeCG は最終的なイメージがウェブページとしてレンダリングされるので、 配信オーバーレイとして使う場合、各配信ソフトからブラウザーインプットによって NodeCG の URL を読み込ませます。

対してデジタルサイネージの場合は、NodeCG の URL をブラウザで開いて全画面モードにしておけばいいわけです。

もうちょっと組み込みっぽくしたい場合は、Webビューだけのクライアントを作るなどしてもいいかもしれません。

この分野のプロプライエタリ製品に Ross Video の XPression があります。 もちろん機能は比べ物にならないくらいリッチで、価格もリッチです。


2. NodeCG をインストールする

NodeCG には Node.js ver18 が必要ですので、予めインストールしておいてください。

最初は NodeCG CLI のインストールから開始です。

npm install -g nodecg-cli

次に NodeCG CLI を使って NodeCG サーバーをセットアップします。サーバーは任意のディレクトリにセットアップ可能です。

mkdir nodecg-server
cd nodecg-server
nodecg setup

セットアップが完了したら npm run start で起動して、 ブラウザで http://localhost:9090/ にアクセスしてみましょう。 NodeCG ダッシュボードが表示されるはずです。

デフォルトでは、パスワード認証が設定されていません。設定方法は後で解説します。

サーバーを停止するには CTRL + C を押してください。


3. バンドルを作成する

バンドルとは、サーバーにインストールできる NodeCG アプリケーションの事です。

nodecg install コマンドを使ってネットで公開されているバンドルをダウンロードして使うことも可能です。 プログラミングが出来ない方はこちらの使い方がメインになろうかと思います。

ここでは独自のバンドルを作成する手順を紹介します。

参考ページ (www.nodecg.dev)

3.1. バンドルスケルトンの生成

NodeCG 公式ドキュメントでは Yeoman というジェネレーターツールを使ってスケルトンを生成することが紹介されているので、 それに倣うことにします。

npm install -g yo generator-nodecg
cd bundles
mkdir geolocation-bundle
cd geolocation-bundle
yo nodecg

ステップ・バイ・ステップで質問されるので、Y or N で回答していきます。ほぼすべてデフォルトでエンターを押せば問題ないですが、 1点、React を使用するかどうか聞かれた際は Y を押してください。

? Would you like to generate this bundle in React? (y/N)

バンドル開発モード NodeCG サーバーの起動

バンドル開発モードの NodeCG サーバーは、ソースコードの変更を監視し、必要に応じてリビルド&ホットリロード、 あるいはサーバー再起動をしてくれる、Web 開発者にはお馴染みのモードです。 いちいちビルドコマンドやサーバー起動コマンドを打つ必要がなく、開発効率が向上します。

バンドル開発モードを起動するにはバンドルルートディレクトリで、以下のコマンドを実行してください。

npm run dev

ブラウザーサイドのソースコード変更は、一部を除いてブラウザー側でホットリロードされます。 サーバーサイドのソースコードに変更が入ると、サーバーが再起動されます。

終了するには CTRL + C を押してください。

3.2. バンドルの仕組み

バンドルは大きく分けて3つのパートから構成されます。

  • Dashboard - NodeCG ダッシュボードに追加される設定画面(フォーム)
  • Extension - サーバーサイドの処理を記述する Node.js スクリプト
  • Graphics - 画面(配信オーバーレイのテキスト、画像、アニメーション、サウンド)のレンダリング

ソースコードは以下のディレクトリに配置されます

  • src/dashboard
  • src/extension
  • src/graphics

先ほどスケルトンを生成する際 React を有効にしたので、Graphics と Dashboard は React で記述されています。

データバインディングは「Replicant」という仕組みを使って行います。 Replicant を使用すると Dashboard, Extension, Graphics の間でリアルタイムにデータをやり取りできます。

バンドルには共通して nodecg というグローバル変数が用意されますので、これを使用して Replicant を操作します。

3.3. Replicant 作成

まず Replicant のスキーマを定義し、Dashboard と Graphics で Replicant を扱うために共通の React Hook を作成します。

Replicant スキーマの作成

作成するバンドルでは、配信オーバーレイで

  • 現在座標がマーキングされたミニマップ
  • 現在地の住所
  • 現在時刻

を表示したいと考えています。

これらのデータを取り扱う Replicant のスキーマを作成しましょう。

スキーマは schemas/ ディレクトリに JSON ファイルとして配置します。 デフォルトでは exampleReplicant.json というファイルがありますので、これを geolocationReplicant.json にリネームします。 フォーマットは JSON Schema であり、今回の目的に合わせて以下の様に修正しました。

{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "type": "object",
    "properties": {
        "timestamp": {
            "description": "Timestamp",
            "type": "integer",
            "minimum": 0
        },
        "position": {
            "description": "Position",
            "type": "array",
            "items": {
                "type": "number"
            }
        },
        "address": {
            "description": "Address",
            "type": "string"
        }
    },
    "additionalProperties": false,
    "required": [ "timestamp" ],
    "default": {
        "timestamp": 0,
        "position": [ 35.686152, 139.752842 ],
        "address": ""
    }
}
  • timestamp - 現在時刻(Unix time,ミリ秒)
  • position - 現在座標の緯度経度(numberの配列)
  • address - 現在地の住所

を、それぞれ格納できるようにしました。デフォルト値はなんでもいいのですが、とりあえず皇居の座標に設定しておきました。

スキーマを編集したら以下のコマンドを実行して、TypeScript のタイプファイルを生成します。

npm run generate-schema-types

src/types/schema にタイプファイルが生成されます。

Replicant アクセス用 React Hook の作成

参考ページ (qiita.com)

src/hooks/GeolocationReplicant.tsx というファイルを作成し、 以下の様に Replicant を取得する React Hook を作成します。

import React, { useCallback, useEffect, useState } from "react";
import type { GeolocationReplicant } from "../types/schemas";


export const useGeolocationReplicant = () => {
    const [ replicant ] = useState(() => nodecg.Replicant<GeolocationReplicant>("geolocationReplicant"));
    const [ geolocation, setGeolocation ] = useState<GeolocationReplicant | undefined>(replicant.value);

    useEffect(() => {
        const handleChange = (newValue?: GeolocationReplicant) => setGeolocation(newValue && { ...newValue });
        replicant.on("change", handleChange);

        return () => {
            replicant.off("change", handleChange);
        };
    }, [ replicant ]);

    return {
        geolocation,
        setGeolocation: useCallback(
            (newValue?: React.SetStateAction<GeolocationReplicant | undefined>) => {
                if (typeof (newValue) === "function") {
                    replicant.value = newValue(replicant.value);
                } else {
                    replicant.value = newValue;
                }
            },
            [ replicant ]
        )
    };
};

冒頭の const [ replicant ] = useState(() => nodecg.Replicant<GeolocationReplicant>("geolocationReplicant"))geolocationReplicant の参照を取得します。

replicant.value プロパティが現在値を表し、代入することで値を更新することが可能です。 value プロパティの型は GeolocationReplicant となっており、 先ほど作成したスキーマの通り、src/type/schemas にタイプファイルが生成されています。

また、Replicant は value プロパティが変化する度に change イベントを発火するので、 これをリッスンするイベントハンドラを useEffect 内で登録(あるいは登録解除)しています。

イベントハンドラでは state 変数 geolocation を最新の値で更新します。 それに伴ってレンダリングが発生しますので Graphics がリアクティブに更新されます。

バグなのか何なのか、timestamp の更新でレンダリングが発生しませんでしたので、 対策として setGeolocation(newValue && { ...newValue }) という様に新しいオブジェクトを生成しています。

newValue の参照が Replicant 内部で使いまわされることがある?様です

フック関数が返す setGelocation 関数は、Replicant の value プロパティを更新するための関数です。 使い易いように useState フックが戻す set??? 関数と同様に、設定用関数を渡すこともできるようにしました。

Replicant の value プロパティの値は Graphics, Extension, Dashboard の間で同期されます。

3.4. アセットカテゴリの定義

「アセット」は NodeCG ダッシュボードで差し替え可能なリソース(画像・音声・動画等)です。 アップロードしたファイルはサーバーに保存され、Replicant としてアクセス URL が取得可能になります。

アセットカテゴリを定義すると、NodeCG ダッシュボードでアップロード可能になります。

今回のバンドルでは、ミニマップの上に表示するマーカーをアセットとして定義します。

package.jsonnodecg オブジェクトに assetCategories 配列を追加します。 marker というアセットカテゴリを追加しました。

透過してほしいので、gifpngsvg のみ許可するようにしました。

{
  ...中略...
  "nodecg": {
    ...中略...
    "assetCategories": [
      {
        "name": "marker",
        "title": "Position marker image",
        "allowedTypes": [
          "gif",
          "png",
          "svg"
        ]
      }
    ]
  },
  ...中略...
}

ここで一度 NodeCG サーバーをバンドル開発モード(バンドルルートディレクトリで npm run dev)で起動してアセットカテゴリが追加されているか確認しましょう。

起動後、Web ブラウザーで ASSETS にアクセスし、 Position marker image アセットカテゴリに何か画像ファイルを一つアップロードしてください。

更に、アセットを取得するための React Hook を作成します。

src/hooks/MarkerAsset.tsx というファイルを以下の内容で作成します。

import type NodeCG from '@nodecg/types';
import { useEffect, useState } from "react";


export const useMarkerAsset = () => {
    const [ asset ] = useState(() => nodecg.Replicant<NodeCG.AssetFile[]>("assets:marker"));
    const [ markers, setMarkers ] = useState(asset.value);

    useEffect(() => {
        const handleChange = (newValue?: NodeCG.AssetFile[]) => setMarkers(newValue && [ ...newValue ]);
        asset.on("change", handleChange);

        return () => {
            asset.off("change", handleChange);
        };
    }, [ asset ]);

    return { markers };
};

実は、アセットも Replicant の一種ですが、名前の形式が必ず asset:*、型が NodeCG.AssetFile[] になります。 Replicant の change イベントハンドラで最新値を取得するのは useGeolocationReplicant フックと同じです。

3.5. Dashboard の作成

次にダッシュボードで設定画面を作成します。

まず、本サンプルでは react-hook-form, bulma, sass を使用するのでバンドルプロジェクトに追加します。 スケルトンではバンドラーとして Parcel が使われているので、 *.scss をコンパイルするために @parcel/transformer-sass も追加します。

npm install react-hook-form bulma
npm install -D @parcel/transformer-sass

CSS フレームワークとして bulma を使うので、src/dashboard/panel.scss を以下の内容で作成します。

@use "bulma";

次に、src/dashboard/Panel.tsx を編集します。

import React, { useCallback, useEffect } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import { useGeolocationReplicant } from "../hooks/GeolocationReplicant";
import { GeolocationReplicant } from "../types/schemas";
import "./panel.scss";


interface FormData {
    position: number[];
    address: string;
}

interface FormProps {
    geolocation?: GeolocationReplicant,
    onSubmit: (values: FormData) => unknown
}

const Form = ({ geolocation, onSubmit }: FormProps) => {
    const {
        register,
        handleSubmit,
        setValue,
        getFieldState,
        formState: { isSubmitting, isValid }
    } = useForm<FormData>({
        defaultValues: {
            position: geolocation?.position,
            address: geolocation?.address,
        }
    });

    useEffect(() => {
        const positionState = getFieldState("position");
        if (geolocation?.position && !positionState.isDirty) {
            setValue("position", geolocation.position);
        }

        const addressState = getFieldState("address");
        if (geolocation?.address && !addressState.isDirty) {
            setValue("address", geolocation.address);
        }
    }, [ geolocation?.position, geolocation?.address ]);

    return (<form onSubmit={ handleSubmit(onSubmit) }>
        <label className="label">
            Position <span className="has-text-danger is-size-7">※必須</span>
        </label>
        <div className="columns is-mobile">
            <div className="column">
                <div className="field">
                    <div className="control">
                        <input
                            className="input"
                            type="number"
                            step="any"
                            required={ true }
                            disabled={ isSubmitting }
                            { ...register("position.0", {
                                required: true,
                                valueAsNumber: true
                            }) }
                        />
                    </div>
                </div>
            </div>
            <div className="column">
                <div className="field">
                    <div className="control">
                        <input
                            className="input"
                            type="number"
                            step="any"
                            required={ true }
                            disabled={ isSubmitting }
                            { ...register("position.1", {
                                required: true,
                                valueAsNumber: true
                            }) }
                        />
                    </div>
                </div>
            </div>
        </div>

        <div className="field">
            <label className="label">
                Address
            </label>
            <div className="control">
                <input
                    className="input"
                    type="text"
                    disabled={ isSubmitting }
                    { ...register("address") }
                />
            </div>
        </div>

        <div className="field is-grouped">
            <button
                className={ `button is-link ${ isSubmitting ? "is-loading" : "" }` }
                type="submit"
                disabled={ isSubmitting || !isValid }
            >送信
            </button>
        </div>
    </form>);
};

export const Panel = () => {
    const { geolocation, setGeolocation } = useGeolocationReplicant();

    const submit: SubmitHandler<FormData> = useCallback(async (data) => {
        console.debug(data.position);
        setGeolocation((prev) => ({
            timestamp: Date.now(),
            ...prev,
            position: data.position,
            address: data.address,
        }));
    }, []);

    return (
        <section className="section">
            <div className="container">
                <div className="content">
                    { geolocation ? <Form geolocation={ geolocation } onSubmit={ submit }/> : null }
                </div>
            </div>
        </section>
    );
};

何の変哲もない、いつもの Web フォームですが、 NodeCG ダッシュボードからは iframe で読み込まれるので、遠慮なく Bulma の CSS クラスを使います。

送信ボタンを押すと setGeolocation 関数で Replicant の値を更新します。

とはいえ、今回作るバンドルでは、モバイル端末からの送信で値を更新するので、本番でこの設定フォームを使うことはあまり想定していません。 現在値がリアルタイムで分かるように、値が変わったらフォームの値も変えるようにしています。

NodeCG のダッシュボードはダークモードのみなので、src/dashboard/panel.html<html> タグに data-theme="dark" を追加して Bulma のテーマをダークモードに設定しました。

<!DOCTYPE html>
<html lang="ja" data-theme="dark">
...以下略...

バンドル開発モード(バンドルルートディレクトリで npm run dev)を起動して、 Web ブラウザーで NodeCG ダッシュボードの WORKSPACE にアクセスして、 設定フォームが表示されていることを確認してください。

Replicant への変更は永続化されます

NodeCG には標準で SQLite が統合されているので、Replicant の値は永続化され、サーバー再起動後も保持されます。 他の DB に保存したい場合は Extension に自前でロード・セーブを実装してください。

3.6. Graphics の作成

次にいよいよ配信オーバーレイの作成に取り掛かります。

おさらいすると、配信画面では

  • 現在座標がマーキングされたミニマップ
  • 現在住所
  • 現在時刻

の 3 つをオーバーレイ表示します。

まず、必要なライブラリをバンドルプロジェクトに追加します。 地図表示には leaflet およびその React コンポーネント版 react-leaflet を使用します。 現在時刻の表示には moment を使います。

npm install leaflet react-leaflet moment
npm install -D @types/leaflet @types/react-leaflet

次に src/graphics/Index.tsx を編集します。 こちらはオーバーレイを表示するコンポーネント本体です。

import Leaflet, { LatLng } from "leaflet";
import moment from "moment";
import React, { useEffect } from "react";
import { MapContainer, Marker, TileLayer, useMap } from "react-leaflet";
import { useGeolocationReplicant } from "../hooks/GeolocationReplicant";
import { useMarkerAsset } from "../hooks/MarkerAsset";
import { GeolocationReplicant } from "../types/schemas";
import "./index.scss";
import "leaflet/dist/leaflet.css";


const MapComponent = ({ geolocation }: { geolocation: GeolocationReplicant }) => {
    const map = useMap();
    const { markers } = useMarkerAsset();

    useEffect(() => {
        if (!geolocation.position) {
            return;
        }
        map.setView(new LatLng(geolocation.position[0], geolocation.position[1]));
    }, [ geolocation.position ]);

    return (<>
        <TileLayer
            attribution='&copy; <a href="https://osm.org/copyright">OpenStreetMap</a> contributors'
            url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />

        { markers?.[0] ? <Marker
            position={ new LatLng(geolocation.position?.[0] ?? 0, geolocation.position?.[1] ?? 0) }
            icon={ Leaflet.icon({
                iconUrl: markers[0].url,
                iconSize: [ 48, 48 ],
                iconAnchor: [ 24, 24 ],
                className: "marker-icon"
            }) }
        /> : null }
    </>);
};

export const Index = () => {
    const { geolocation } = useGeolocationReplicant();

    return geolocation ? (
        <section className="section">
            <div className="container">
                <div className="content">
                    <div className="stream-overlay">
                        <MapContainer
                            center={ new LatLng(
                                geolocation.position?.[0] ?? 0,
                                geolocation.position?.[1] ?? 0
                            ) }
                            zoom={ 15 }
                            scrollWheelZoom={ false }
                            zoomControl={ false }
                        >
                            <MapComponent geolocation={ geolocation }/>
                        </MapContainer>

                        <div className="timestamp">
                            <span className="is-size-1 has-text-white">
                                { moment(geolocation.timestamp).format("YYYY/MM/DD HH:mm") }
                            </span>
                        </div>

                        <div className="address">
                            <span className="is-size-2 has-text-white">{ geolocation.address }</span>
                        </div>
                    </div>
                </div>
            </div>
        </section>
    ) : null;
};

geolocation に Replicant のリアルタイム値が格納されるので、これを表示に使用します。

  • geolocation.timestamp - 現在時刻(Unix time,ミリ秒)
  • geolocation.position - 現在座標の緯度経度(numberの配列)
  • geolocation.address - 現在地の住所

ミニマップの中心座標は useMap フックで取得できる map オブジェクトの setView メソッドで設定します (MapContainer コンポーネントの center プロパティは初期値)

ミニマップの真ん中にアセットから取得したマーカー画像を表示します。 アセットカテゴリにファイルが複数アップロードされていた場合は、先頭のファイルを使用します。

冒頭でインポートしているスタイルシート src/graphics/index.scss を以下の内容で作成しました。

@use "bulma";
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');

html {
  background-color: transparent !important;
  overflow: hidden;
  body {
    font-family: 'Noto Sans JP', sans-serif;
  }
}

.stream-overlay {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;

  .leaflet-container {
    position: absolute;
    width: 400px;
    height: 400px;
    bottom: 45px;
    right: 45px;
  }

  .timestamp {
    position: absolute;
    top: 45px;
    left: 0;
    margin-left: 45px;
    margin-right: 45px;
  }

  .address {
    position: absolute;
    bottom: 45px;
    left: 0;
    margin-left: 45px;
    margin-right: 490px;
  }
}

配信オーバーレイとして作ってるので、上記スタイルは完全にレスポンシブルではなく、 ブラウザーサイズが 1920 x 1080 固定であることを前提にしています。

バンドル開発モード (バンドルルートディレクトリで npm run dev) を起動して、 Web ブラウザーで NodeCG ダッシュボードの GRAPHICS にアクセスして、 追加した Graphics がリストに表示されていることを確認してください。

「INDEX.HTML」のリンクをクリックすると、Web ブラウザでオーバーレイ表示を確認できます。

Replicant 更新のテストもしましょう。

WORKSPACE で、Position の値を、 例えば 38.897859, -77.036583 に変更して送信し、ミニマップがホワイトハウスに移動することを確認してください。

Address に適当な文字列を入力して「送信」し、画面下部に表示されることを確認してください。

背景を透明に設定しているので、Web ブラウザでは背景が白くなって文字が見えないかもしれません。 Ctrl + A でテキストを全選択するなどしてください。

サンプルスクリーンショットでは見易くするために、無理やり背景を黒くしています。

3.7. Extension の作成

サーバーサイドの処理を src/extension に実装します。

  1. 現在時刻の更新 (必要性はないがサンプルなのでサーバー側で生成)
  2. 現在座標、現在地住所の更新 (REST API)

を行います。

src/extension/index.ts を以下の内容で編集します。

import type NodeCG from "@nodecg/types";
import type { GeolocationReplicant } from "../types/schemas";

module.exports = function (nodecg: NodeCG.ServerAPI) {
    const geolocationReplicant = nodecg.Replicant<GeolocationReplicant>("geolocationReplicant");
    setInterval(() => {
        if (geolocationReplicant.value) {
            geolocationReplicant.value.timestamp = Date.now();
        }
    }, 10000);

    const router = nodecg.Router();
    router.use(nodecg.util.authCheck);

    router.post("/position", (req, res) => {
        if (!Array.isArray(req.body.position) || req.body.position?.[0] == null || req.body.position?.[1] == null) {
            return res.sendStatus(400);
        }
        if (geolocationReplicant.value) {
            geolocationReplicant.value.position = [ Number(req.body.position[0]), Number(req.body.position[1]) ];
        }
        res.sendStatus(204);
    });

    router.post("/address", (req, res) => {
        if (geolocationReplicant.value) {
            geolocationReplicant.value.address = req.body.address ?? "";
        }
        res.sendStatus(204);
    });

    nodecg.mount("/geolocation", router);
};

現在時刻 timestamp は単に setInterval で 10 秒毎に更新します。

座標と住所を更新する REST API は カスタムルート で作成します。

実は NodeCG サーバーは Express が基盤になっているので nodecg.Router() でルーティングを追加できます。

router.post("/position", ...)router.post("/address", ...) 二つのエンドポイントを作成し、 nodecg.mount("/geolocation", router)/geolocation にマウントしました。

Router の使い方は Express そのものであり、 処理内容はどちらのエンドポイントも、簡単にリクエストをバリデーションして Replicant を更新するだけです。

現在はエンドポイントに認証も何もない状態ですが、後でセキュリティを追加します。

バンドル開発モード (バンドルルートディレクトリで npm run dev) を起動して、以下のコマンドで動作を確認してください。

curl -X POST -H "Content-Type: application/json" -d '{"position":[38.897859,-77.036583]}' http://localhost:9090/geolocation/position
curl -X POST -H "Content-Type: application/json" -d '{"address":"1600 Pennsylvania Ave NW, Washington, DC 20500"}' http://localhost:9090/geolocation/address`

エラー無くコマンドが終了したら、 Web ブラウザーで NodeCG ダッシュボードの GRAPHICS にアクセスして、 表示内容が更新されるかを確認してください。

また、WORKSPACE でも、PositionAddress の値が更新されているはずです。

このように Replicant を更新すると、即座に Dashboard 及び Graphics に反映されるため、 リアルタイムなデータバインディングが容易に実装出来ます。

4. セキュリティの追加

このままだと、誰でも NodeCG ダッシュボードにアクセスできてしまい、 通信経路も暗号化されていないので、パスワード認証、及び SSL 通信を導入します。

4.1. パスワード認証

参考ページ (www.nodecg.dev)

ここからは、NodeCG サーバールートディレクトリに移動して作業します。

www.luft.co.jprandomkeygen.com 等を利用して、 32 文字程度のランダムな文字列を 1 つ生成しておきます。

パスワードは HMAC でハッシュ化して保存するので、HMAC Generator を使用しました。 Secret Key には上記で生成したランダム文字列、Hash Typesha256 を選択します。

NodeCG サーバー設定ファイル cfg/nodecg.json を作成し、以下の内容としました。 (シークレット・パスワードを含むので一部伏字 *** にしています)

{
  "login": {
    "enabled": true,
    "sessionSecret": "********",
    "local": {
      "enabled": true,
      "allowedUsers": [
        {
          "username": "admin",
          "password": "sha256:********"
        }
      ]
    }
  }
}
  • sessionSecret - 生成したランダム文字列
  • password - HMAC でハッシュ化したパスワードに sha256: を付けたもの

NodeCG サーバーを再起動すると、ログイン画面が表示されるようになります。

パスワード認証を有効にすると、Graphics の INDEX.HTML へのアクセスもセキュリティ保護されます。 NodeCG ダッシュボードの GRAPHICS で取得する URL には NodeCG Key が付加 (?key=****) された状態となります。

セキュリティ保護以前にコピーした、NodeCG Key が付加されていない URL ではアクセスできなくなるので注意してください。

REST API もセキュリティ保護されます

Extension のカスタムルートをセットアップするコードに

router.use(nodecg.util.authCheck);

の一行が含まれていますが、こちらでカスタムルートもセキュリティ保護が掛かった状態となります。 アクセスする為に NodeCG Key の付加 (?key=****) が必要となります。

4.2. SSL 通信有効化

SSL を使用しない場合はこの手順を飛ばしてください。

参考ページ (www.nodecg.dev)

先に独自ドメイン (DDNS でも可) と、Let's Encrypt で独自ドメイン等の SSL 証明書を取得します。 独自ドメインの説明は省略します。Let's Encrypt の SSL 証明書取得は certbot を使うと手っ取り早いです。

ご使用のルーターが対応している場合は、DDNS と Let's Encrypt の自動更新を設定すると良いでしょう。

certbot を使用する前に作業 PC の 80 番 TCP ポートを解放してください。 何らかの Web サーバーが 80 番ポートで起動している場合は、certbot と競合するので一時的に停止してください。

certbot をインストールしたら以下のコマンドを実行します (your-dmaon は独自ドメイン名に置き換えてください)

certbot certonly --standalone -d your-domain

Windows の場合、C:\Certbot\live\your-domain に証明書が保存されます。 Linux の場合は、/etc/letsencrypt/live/your-domain に保存されます。

NodeCG サーバー設定ファイル cg/nodecg.json に以下の内容を追加します。

{
  ...中略...
  "ssl": {
    "enabled": true,
    "allowHTTP": false,
    "keyPath": "C:\\Certbot\\live\\your-domain\\privkey.key",
    "certificatePath": "C:\\Certbot\\live\\your-domain\\fullchain.key"
  }
}

NodeCG サーバーを再起動すると、SSL でアクセス可能となります (https://your-domain:9090)

引き続き http://localhost:9090 でアクセスしたい場合は allowHTTPtrue に設定してください。


5. ビルドと NodeCG サーバー起動

TypeScript を使用してバンドルを作成した場合、NodeCG サーバーを起動する前にバンドルのビルドが必要です。

バンドル開発モードを起動するとビルドされますが、本番環境ではビルドを明示的に行う必要があります。

バンドルルートディレクトリで以下のコマンドを実行します。

npm run build

NodeCG サーバーを起動するには、NodeCG サーバールートディレクトリに移動して以下のコマンドを実行します。

npm run start

6. モバイル端末から位置情報を送信

モバイル端末から位置情報を送信する為に Automate というアプリをインストールします。ちなみに今回は Android 端末を想定します。

Automate はフローチャート形式でスクリプトを作成できるアプリです。

尚、インターネットにアクセスするので Automate network permissions もインストールが必要になります。

説明は省きますが iOS でも「ショートカット」を使えば同じことができます。

6.1. NodeCG Key を取得

パスワード認証を有効にした場合は、REST API も認証が必要になります。

Web ブラウザーで NodeCG ダッシュボードの SETTINGS にアクセスし、 「SHOW KEY」をクリックして、NodeCG Key を表示させてください。

この NodeCG Key を使用した REST API のエンドポイント URI は以下の通りとなります。

  • http://your-domain:9090/geolocation/position?key=NodeCGKey
  • http://your-domain:9090/geolocation/address?key=NodeCGKey

your-domain は独自ドメイン名に、NodeCGKey は表示された NodeCG Key にそれぞれ置き換えてください。 また、SSL 接続の場合は http の代わりに https を使用してください。

6.2. Automate でスクリプトを作成

Automate を起動し、以下のフローを作成しました。

ファイルを こちら からダウンロードできるので、Automate にインポートしてください。

次に、インポートしたフローを開いて編集し、HTTP request の「URL」を、独自ドメイン及び NodeCG Key 付の物に差し替えてください。

住所の文言は Set variable message で定義しているので、お好みで変更してください。

6.3. Automate フロー起動

このフローでは以下の二つの Starting point があります。

  • GPS から現在地を取得して住所に変換し、テキストで NodeCG に送信する (1 分間隔で実行)
  • GPS から現在地を取得して座標を NodeCG に送信する (10 秒間隔で実行)

フローを開いて「START」ボタンをタップすると、 Starting point を選択するダイアログが表示されるので「START ALL」をタップして起動してください。

NodeCG サーバーにインターネットからアクセスするので、サーバーの 9090 TCP ポートを解放しておいてください。

うまくいけば、REST API 経由で NodeCG の Replicant がアップデートされ、オーバーレイが更新される事でしょう!

尚、このフローは一度「START」すると、「STOP」を押すまで(バックグラウンドであっても)稼働し続けます。

以上で、ドライブ配信でリアルタイムの位置情報を、配信オーバーレイに表示させる仕組みが完成しました。


7. 配信ソフトでの合成

最後に、配信画面に合成する仕込みを、各配信ソフトで行いましょう。

と、言ってもブラウザーインプットを作成するだけです。

7.1. Graphics URL の取得

まず、Web ブラウザーで NodeCG ダッシュボードの GRAPHICS にアクセスして、 「COPY URL」をクリックし、メモ帳などにペーストしておいてください。

7.2. OBS Studio の場合

  1. 「ソース」ドック→「プラス記号」→「ブラウザ」をクリック
  2. 新規作成 を選択し、適当な名前 (例:NodeCG) を決めて入力し「OK」をクリック
  3. 「URL」にメモ帳に控えておいた URL をコピー&ペースト
  4. 「幅」を 1920、「高さ」を 1080 に設定
  5. 「カスタムCSS」を空欄にする
  6. 「OK」ボタンをクリック
  7. 「ソース」ドック内で NodeCG ソースをドラッグして他のソースよりも上位に移動

カスタム CSS を使用しなくても、Web ページの背景が透明なら、そのまま透過として扱われます。

7.3. vMix の場合

  1. 画面左下の「Add Input」ボタンをクリック
  2. 左メニューから「Web Browser」を選択
  3. 「URL」にメモ帳に控えておいた URL をコピー&ペースト
  4. 「Width」を 1920、「Height」を 1080 に設定
  5. 「OK」ボタンをクリック
  6. オーバーレイを配置したいインプットの Layers に追加、あるいは NodeCG インプットの「1」~「4」をクリックして Overlay として表示。

vMix の Web Browser インプットでは、Web ページの背景が透明なら、そのまま透過として扱われます。

7.4. 注意点

もし NodeCG サーバー起動中にバンドルを更新してビルドしてもオーバーレイの表示は更新されません。

表示を更新する場合は NodeCG ダッシュボードの GRAPHICS で「RELOAD」をクリックしますが、オーバーレイが一瞬だけ非表示になります。 従ってこれらの操作は別の画面(蓋絵等)に切り替えて行うべきです。


8. ソースコード

今回作成したバンドルのソースコードを以下の GitHub リポジトリにて公開しています。 リポジトリを NodeCG サーバーの bundles ディレクトリ配下に clone することで使用可能です。