feature image

2023年8月21日 | ブログ記事

名取さなになりたくてOBSと連携する配信画面を作った

この記事はtraP 夏のブログリレー 1日目の記事です。夏、はじまれ。


こんにちは、@d_etteiu8383です。最近は@eyemono.moeと名乗ったりしてます。

名取さなになりたくて配信画面を作ったので、名取さなになりたくて配信画面を作った話をします。

0:00
/
今回作成した配信画面の動作デモ

ブラウザで動作するウェブページとして作成し、OBSにウィンドウキャプチャで表示することで配信画面として利用できます。WebSocketによるOBSとの通信により、OBSの音量調整・OBS上のアイテムの位置調整といった操作も可能になっています。

背景

名取さな さんというVtuberがいます。この方の最近の雑談配信における配信画面はウィンドウシステムを模したデザインになっています。

ウィンドウを模したフレームがかわいくてかっこいい。いいな~~

私もこんな配信画面が欲しくなったので作りました。特に配信をする予定はありませんが、いざという時のために配信画面の一つや二つは持っておきたいですね。

デスクトップを模した配信画面を作るということで、せっかくなのでウィンドウを動かせたりできると楽しそうです。

方法

戦略

背景の作り方として以下の方法が上げられます。

  1. 画像で作る
    • 簡単
    • 動かない
      • 面白くない
  2. ウェブページとして作成し、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!
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

— Vite ⚡ (@vite_js) July 6, 2023

ウィンドウシステム

目標は名取さなさんの配信背景です。デスクトップ画面を模してデザインしたいですね。
ということでまずはウィンドウを作りました。

HTMLで作成されたウィンドウ
基本のウィンドウ

位置操作用の"フチ"要素9個をgridで配置し、これの下に重ねる形で"見た目"要素を配置しています。

ウィンドウの辺と角の位置にdiv要素を配置している (見やすさのため実際より太く表示)

この"フチ"要素のonpointermoveイベントを監視してウィンドウをごりごり動かしています。

0:00
/
マウス操作によるウィンドウの位置・サイズ変更デモ

ウィンドウの色や名前、アイコンも設定できるようにしています。画像背景ではできないことですね。

0:00
/
ウィンドウのタイトル・アイコン・色変更デモ

なおポップアップメニューの表示にはFloating UI(のSolid.jsバインディング)を、絵文字ピッカーにはPicMoを使用しています。こうしたライブラリを使用した開発ができるのも、ウェブページとして作成しているからこそですね。

タスクバー

ウィンドウができました。1つだけでは寂しいのでいっぱいウィンドウを作りましょう。タスクバー君の出番です。

画面下部にタスクバーを追加した

ウィンドウの状態を配列で持ち、ウィンドウの追加や削除ができるようになりました。
ウィンドウの最小化や、z-indexの変更による前後関係の変更も実装しています。

0:00
/
ウィンドウの追加・最小化デモ

私自身普段はWindowsのタスクバーを画面左端に表示しているのですが、あまりメジャーな配置ではないので今回は涙を飲んで画面下に配置しました。

時計

Vtuberの配信でめっちゃ見るアレ。タスクバーに置いちゃいましょう。mooncape様が配布されているものが有名ですが、せっかくなので自作します。

画面右下、タスクバー右端に時計を追加した

甘えずrequestAnimationFrameを使って実装し、1/60秒単位で正確に時を刻みましょう。配信遅延?知らねぇよ。

メモ帳

ウィンドウの基本的な仕組みができたので、ここからはウィンドウの中身を作っていきます。

まずはメモ帳です。OBS単体でもテキストの表示はできますが、オプションが多かったり複数回のクリックが必要だったりと少し使いづらいです。
ここではごくシンプルなメモ帳を作りました。

0:00
/
メモウィンドウのデモ

文字編集, サイズ編集とアラインメント設定だけ。十分。色変えたりフォント変えたりは欲しくなったら実装します。

ストップウォッチ

使う予定はありませんがせっかくなので作りました。

0:00
/
ストップウォッチウィンドウのデモ

甘えずrequestAnimationFrameを使って実装し、1/60秒単位で正確に時を刻みましょう。(なお15FPSで録画している)

ペイント

メモ程度にお絵描きしたいときに使えるペイントウィンドウも作りました。

ペイントウィンドウのデモ

MS Paintのほうが高機能ではある。

OBS音声コントローラー

いろいろなものが配信背景上で動くようになってきました。

せっかくなので配信背景からOBSを動かしたいですね。外部ブラウザで表示した配信背景からOBSを操作だと...?そんなことが......?

できます。なんとOBSはWebSocketで操作できます。便利だ。

GitHub - obsproject/obs-websocket: Remote-control of OBS Studio through WebSocket
Remote-control of OBS Studio through WebSocket. Contribute to obsproject/obs-websocket development by creating an account on GitHub.

ということで作りました。配信背景からOBSの音量を変更できるようになりました。

0:00
/
音量操作のデモ

今回はobs-websocket-community-projects/obs-websocket-jsを用いて実装しました。楽ちん~

GitHub - obs-websocket-community-projects/obs-websocket-js: Consumes https://github.com/obsproject/obs-websocket
Consumes https://github.com/obsproject/obs-websocket - GitHub - obs-websocket-community-projects/obs-websocket-js: Consumes https://github.com/obsproject/obs-websocket
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;

大体こんな感じで実装しています。(非同期処理苦手すぎてあまりきれいな設計ではないしよく見たらエラーハンドリングできてない)

これで配信背景からOBSの音量を変更できるようになりました。

クロマキー

ウィンドウの中を一色に塗りつぶせるようにしたことで、クロマキーが使えるようになりました。任意BBが作れます。色も変えられるので任意GBが欲しい時も安心です。

マウスカーソル君BB.png

配置の同期

前述のクロマキーを使用して、OBSのフィルターで配信背景を透過できるようになりました。透過させた領域にゲーム画面キャプチャやコメント欄キャプチャを重ねればいよいよ配信画面として使えそうです。

OBSに取り込んだ配信背景をクロマキーフィルタで透過している

しかし、配信画面を作る際に 配信背景側のウィンドウ と OBS側のキャプチャ等アイテム の位置を手動で合わせる必要があります。めんどくさ。

ということで、配信背景のウィンドウ枠に自動でOBS側のアイテムの位置サイズ前後関係を調製する機能を実装しました。下記動画デモで、OBS側でのアイテム位置調整等は行っていません。

0:00
/
ブラウザで表示した配信背景のウィンドウにOBS側のアイテム位置を同期させるデモ

WebSocket万歳。

ちなみに動く私のアイコンは https://you.eyemono.moe をキャプチャしています。

you.svg - 四十物さんは見ている
あなたの顔を眺めるページ

このアイコンについても以前ブログ記事として投稿しています。ぜひご覧ください↓

【MediaPipe + KalidoKit】ブラウザでリアルタイム顔認識して遊ぶ
こんにちは、@d_etteiu8383です。この記事は夏のブログリレー 2022年 28日目の記事です。 概要 本記事ではMediaPipeのFaceMeshとKalidoKitを利用して、ブラウザ上でリアルタイムに顔認識を行う方法について説明します。 イントロ 突然ですが本日9月6日が何の日だかご存じでしょうか?そうです、真中のんさんの誕生日ですね。 プリパラ 第1話「アイドル始めちゃいました!」 彼女はアニメ『プリパラ』の登場人物の一人ですが、このアニメは3DCGを利用したライブシーンが特徴的ですよね。3Dと言えば&lt;!-- ここにこじつけ理由を書く --&gt;というわけで本記…

ログ

せっかくデスクトップ画面っぽく作ったので、パソコンっぽいウィンドウが欲しくなりました。ターミナルっぽいものが欲しいですね。作りました。

ログウィンドウのデモ

適当なロガーを自作し、logger.loglogger.errorを呼ぶことでこのログ画面にログを表示できるようにしました。devtool開かなくてもデバッグできるようになって普通に便利でした。

ついでにこの配信背景システムに名前を付けました。eyeOSです。私の名前はeyemonoです。

ちょっと調べてみたらどうやらeyeOSは既に存在していたようです(しかもウェブデスクトップ)。本ブログ執筆中に知りました。ごめんなさい。

ロード画面

さて、実はウィンドウが動くだとかOBSを操作できるだとかはどうでも良くて、一番私が作りたかった画面がこのロード画面です。

Virtual YouTuberのロード画面 何をロードしているのか問題」に対する回答を、私は持っておきたかったのです。

ウェブアプリとして配信背景を作ったため、ロード画面で真にロードをすることができます

0:00
/
ロード画面のデモ

これで堂々と、胸を張って、「私のロード画面ではhtml, css, js, その他依存するリソースをロードしています」と言えます。安心ですね。

まとめ

ブラウザで動作するウェブページとして配信背景を作成しました。
WebSocketによるOBSとの通信により、OBSの音量調整・OBS上のアイテムの位置調整といった操作も可能になっています。
皆さんもぜひ自分だけの配信背景を作ってみてはいかがでしょうか。

名取さなになりたくてOBSと連携する配信画面を作った

d_etteiu8383 icon
eyemono.moe (アイモノ モエ)
チャンネル登録者数 579人
2,434 回視聴 2023/08/21
名取さなにはなれませんでした
夏のブログリレーPlaylist: https://trap.jp/tag/summer-blog-relay/
前回:ないです
次回:コミックマーケット102参戦記 by @shogotin
d_etteiu8383 icon
この記事を書いた人
d_etteiu8383

グラフィック班とゲーム班とSysAd班所属 いろいろ活動しています

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2021年8月12日
CPCTFを支えたWebshell
mazrean icon mazrean
2021年5月19日
CPCTF2021を実現させたスコアサーバー
xxpoxx icon xxpoxx
2022年9月26日
競プロしかシラン人間が web アプリ QK Judge を作った話
tqk icon tqk
2022年9月16日
5日でゲームを作った #tararira
Komichi icon Komichi
2024年4月14日
Spotifyのクライアントを自作しよう
d_etteiu8383 icon d_etteiu8383
2024年3月15日
個人開発として2週間でWebサービスを作ってみた話 〜「LABEL」の紹介〜
Natsuki icon Natsuki
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記