この記事はtraP 夏のブログリレー 1日目の記事です。夏、はじまれ。
こんにちは、@d_etteiu8383です。最近は@eyemono.moeと名乗ったりしてます。
名取さなになりたくて配信画面を作ったので、名取さなになりたくて配信画面を作った話をします。
↑今回作成した配信画面の動作デモ
ブラウザで動作するウェブページとして作成し、OBSにウィンドウキャプチャで表示することで配信画面として利用できます。WebSocketによるOBSとの通信により、OBSの音量調整・OBS上のアイテムの位置調整といった操作も可能になっています。
背景
名取さな さんというVtuberがいます。この方の最近の雑談配信における配信画面はウィンドウシステムを模したデザインになっています。
ウィンドウを模したフレームがかわいくてかっこいい。いいな~~
私もこんな配信画面が欲しくなったので作りました。特に配信をする予定はありませんが、いざという時のために配信画面の一つや二つは持っておきたいですね。
デスクトップを模した配信画面を作るということで、せっかくなのでウィンドウを動かせたりできると楽しそうです。
方法
戦略
背景の作り方として以下の方法が上げられます。
- 画像で作る
- 簡単
- 動かない
- 面白くない
- ウェブページとして作成し、OBS内ブラウザソースで表示する
- 動かせる
- 面白い
- 機能が弱い
- Chromium Embedded Framework (CEF)によるブラウザを搭載しているが、バージョンがちょっと古い
- select要素でoptionが表示されない(致命的)
- input要素で日本語が入力できない(致命的)
- 動かせる
始めはOBS内ブラウザソースで表示することを前提に作っていましたが、色々と不便なので諦めます。
普通に外部ブラウザで表示してウィンドウキャプチャするのが一番手軽で実用的であると判断しました。
ということで後はHTMLとJSとCSSをこねこねするだけです。
実装
私の大好きなSolid.jsで実装しました。
なんと、7月にリリースされたvite@4.4.0からnpm create vite@latest
でSolid.jsテンプレートを選択できるようになっています!
風・・・なんだろう吹いてきてる確実に、着実に、Solid.jsのほうに。
⚡️ vite@4.4.0 is out!
— Vite ⚡ (@vite_js) July 6, 2023
Right on time to celebrate 800 contributors to Vite Core! 🙌
🏁 Experimental @lightningcss support
⬆️ esbuild 0.18
💜 create-vite starters for @solid_js and @QwikDev
💬 Korean translation for Vite's docs
Read the full changelog at https://t.co/O9DuKhGHYP
ウィンドウシステム
目標は名取さなさんの配信背景です。デスクトップ画面を模してデザインしたいですね。
ということでまずはウィンドウを作りました。
位置操作用の"フチ"要素9個をgridで配置し、これの下に重ねる形で"見た目"要素を配置しています。
この"フチ"要素のonpointermove
イベントを監視してウィンドウをごりごり動かしています。
↑マウス操作によるウィンドウの位置・サイズ変更デモ
ウィンドウの色や名前、アイコンも設定できるようにしています。画像背景ではできないことですね。
↑ウィンドウのタイトル・アイコン・色変更デモ
なおポップアップメニューの表示にはFloating UI(のSolid.jsバインディング)を、絵文字ピッカーにはPicMoを使用しています。こうしたライブラリを使用した開発ができるのも、ウェブページとして作成しているからこそですね。
タスクバー
ウィンドウができました。1つだけでは寂しいのでいっぱいウィンドウを作りましょう。タスクバー君の出番です。
ウィンドウの状態を配列で持ち、ウィンドウの追加や削除ができるようになりました。
ウィンドウの最小化や、z-index
の変更による前後関係の変更も実装しています。
↑ウィンドウの追加・最小化デモ
私自身普段はWindowsのタスクバーを画面左端に表示しているのですが、あまりメジャーな配置ではないので今回は涙を飲んで画面下に配置しました。
時計
Vtuberの配信でめっちゃ見るアレ。タスクバーに置いちゃいましょう。mooncape様が配布されているものが有名ですが、せっかくなので自作します。
甘えずrequestAnimationFrame
を使って実装し、1/60秒単位で正確に時を刻みましょう。配信遅延?知らねぇよ。
メモ帳
ウィンドウの基本的な仕組みができたので、ここからはウィンドウの中身を作っていきます。
まずはメモ帳です。OBS単体でもテキストの表示はできますが、オプションが多かったり複数回のクリックが必要だったりと少し使いづらいです。
ここではごくシンプルなメモ帳を作りました。
文字編集, サイズ編集とアラインメント設定だけ。十分。色変えたりフォント変えたりは欲しくなったら実装します。
ストップウォッチ
使う予定はありませんがせっかくなので作りました。
甘えずrequestAnimationFrame
を使って実装し、1/60秒単位で正確に時を刻みましょう。(なお15FPSで録画している)
ペイント
メモ程度にお絵描きしたいときに使えるペイントウィンドウも作りました。
MS Paintのほうが高機能ではある。
OBS音声コントローラー
いろいろなものが配信背景上で動くようになってきました。
せっかくなので配信背景からOBSを動かしたいですね。外部ブラウザで表示した配信背景からOBSを操作だと...?そんなことが......?
できます。なんとOBSはWebSocketで操作できます。便利だ。
ということで作りました。配信背景からOBSの音量を変更できるようになりました。
今回はobs-websocket-community-projects/obs-websocket-jsを用いて実装しました。楽ちん~
useOBSWebSocket.tsimport OBSWebSocket, { EventSubscription } from "obs-websocket-js";
import { createResource } from "solid-js";
import { createStore } from "solid-js/store";
/** OBS WebSocketのインスタンス */
export const globalOBSWebsocket = new OBSWebSocket();
/** OBS接続に必要な情報 */
const [obsConfig, setObsConfig] = createStore("obsConfig", {
address: "ws://localhost:4455",
password: "",
});
/**
* OBS WebSocketに接続する
*
* @param config 接続先のアドレスとパスワード
* @returns 接続に成功したかどうか
*/
const connect = async (data: { address: string; password: string }) => {
console.log("Connecting to OBS WebSocket...");
// 既に接続済みの場合は一度切断する
if (globalOBSWebsocket.identified) {
console.log("already connected, disconnecting...");
await globalOBSWebsocket.disconnect();
}
try {
const { obsWebSocketVersion, negotiatedRpcVersion } =
await globalOBSWebsocket.connect(data.address, data.password, {
rpcVersion: 1,
eventSubscriptions: EventSubscription.All,
});
console.log(
`Connected to server ${obsWebSocketVersion} (using RPC ${negotiatedRpcVersion})`,
);
} catch (error) {
console.error("Failed to connect");
}
return globalOBSWebsocket.identified;
};
const [obsConnected, { refetch: refetchObs }] = createResource(
() => ({
address: obsConfig.address,
password: obsConfig.password,
}),
connect,
);
export const useObsWebSocket = () => ({
globalOBSWebsocket,
obsConnected,
obsConfig,
actions: {
refetchObs,
setObsConfig,
},
});
useInputVolume.tsimport { type Accessor, createResource } from "solid-js";
import { useObsWebSocket } from "../contexts/useObsWebSocket";
const { globalOBSWebsocket, obsConnected } = useObsWebSocket();
/**
* Hook to get and set the volume of an OBS input
*
* @param inputName The name of the input to get/set the volume of
* @returns An object containing the current volume and a function to set the volume
*/
const useInputVolume = (inputName: Accessor<string>) => {
const getInputVolume = async (): Promise<number> => {
if (obsConnected.state !== "ready" || !obsConnected()) {
console.error(`OBS not connected, cannot get ${inputName()} volume`);
return 0;
}
try {
const res = await globalOBSWebsocket.call("GetInputVolume", {
inputName: inputName(),
});
return res.inputVolumeDb;
} catch (e) {
console.error(`Error getting ${inputName()} volume:`, e);
return 0;
}
};
const [volume, { refetch, mutate }] = createResource(
// update when obsConnected state changes
() => obsConnected.state === "ready",
getInputVolume,
);
const setVolume = async (volumeDb: number) => {
const inputVolumeDb = Math.max(-100, Math.min(26, volumeDb));
await globalOBSWebsocket.call("SetInputVolume", {
inputName: inputName(),
inputVolumeDb,
});
await refetch();
};
const handleInputVolumeChanged = (data: {
inputName: string;
inputVolumeMul: number;
inputVolumeDb: number;
}) => {
if (data.inputName === inputName()) {
mutate(data.inputVolumeDb);
}
};
globalOBSWebsocket.on("InputVolumeChanged", handleInputVolumeChanged);
return { volume, setVolume };
};
export default useInputVolume;
大体こんな感じで実装しています。(非同期処理苦手すぎてあまりきれいな設計ではないしよく見たらエラーハンドリングできてない)
InputVolumeChanged
イベントを監視して、リアルタイムに音量情報を取得(OBS → 配信背景)SetInputVolume
で音量を変更(配信背景 → OBS)
これで配信背景からOBSの音量を変更できるようになりました。
クロマキー
ウィンドウの中を一色に塗りつぶせるようにしたことで、クロマキーが使えるようになりました。任意BBが作れます。色も変えられるので任意GBが欲しい時も安心です。
配置の同期
前述のクロマキーを使用して、OBSのフィルターで配信背景を透過できるようになりました。透過させた領域にゲーム画面キャプチャやコメント欄キャプチャを重ねればいよいよ配信画面として使えそうです。
しかし、配信画面を作る際に 配信背景側のウィンドウ と OBS側のキャプチャ等アイテム の位置を手動で合わせる必要があります。めんどくさ。
ということで、配信背景のウィンドウ枠に自動でOBS側のアイテムの位置・サイズ・前後関係を調製する機能を実装しました。下記動画デモで、OBS側でのアイテム位置調整等は行っていません。
↑ブラウザで表示した配信背景のウィンドウにOBS側のアイテム位置を同期させるデモ
WebSocket万歳。
ちなみに動く私のアイコンは https://you.eyemono.moe をキャプチャしています。
このアイコンについても以前ブログ記事として投稿しています。ぜひご覧ください↓
ログ
せっかくデスクトップ画面っぽく作ったので、パソコンっぽいウィンドウが欲しくなりました。ターミナルっぽいものが欲しいですね。作りました。
適当なロガーを自作し、logger.log
やlogger.error
を呼ぶことでこのログ画面にログを表示できるようにしました。devtool開かなくてもデバッグできるようになって普通に便利でした。
ついでにこの配信背景システムに名前を付けました。eyeOSです。私の名前はeyemonoです。
ちょっと調べてみたらどうやらeyeOSは既に存在していたようです(しかもウェブデスクトップ)。本ブログ執筆中に知りました。ごめんなさい。
ロード画面
さて、実はウィンドウが動くだとかOBSを操作できるだとかはどうでも良くて、一番私が作りたかった画面がこのロード画面です。
「Virtual YouTuberのロード画面 何をロードしているのか問題」に対する回答を、私は持っておきたかったのです。
ウェブアプリとして配信背景を作ったため、ロード画面で真にロードをすることができます。
これで堂々と、胸を張って、「私のロード画面ではhtml, css, js, その他依存するリソースをロードしています」と言えます。安心ですね。
まとめ
ブラウザで動作するウェブページとして配信背景を作成しました。
WebSocketによるOBSとの通信により、OBSの音量調整・OBS上のアイテムの位置調整といった操作も可能になっています。
皆さんもぜひ自分だけの配信背景を作ってみてはいかがでしょうか。