😄

2ヶ月で作り上げたWebSocket同時接続3000超のライブ演出システム

に公開

LiveFx プロジェクト開発記録

双方向インタラクティブシステムの構築とその軌跡

本稿では、筆者が実際に参画した「LiveFx」プロジェクトについて、その開発経緯と成果、そして得られた知見を共有します。LiveFx は、ライブパフォーマンスにおける演者と観客間のインタラクションを促進するために開発されたデジタル演出ツールです。

プロジェクト概要

LiveFX は、「ライブパフォーマンス向けの双方向インタラクティブシステム」として、専門学校グループの入学式イベントでの利用を目的として開発されました。 本システムの主要な特徴は、演者側の MIDI コントローラーによる画面演出の操作を、リアルタイムに観客のスマートフォンへ視覚効果として同期表示させる点にあります。

このシステムは、専門学校グループの入学式を対象とし、16 の専門学校グループ、最大 6,000 台の観客用スマートフォンへの同時配信を想定したスケーラブルな設計が求められました。

本稿では、この学生主体となった貴重な開発経験を振り返り、その経緯や所感を共有することを目的とします。また、記録してきた写真やソースコードの一部を、公開可能な範囲で提示します。将来的には、ソースコードを OSS として公開できるようにしたいと思っています。

ご協力いただいた皆様には、改めて深く感謝申し上げます。

ongoing

プロジェクト目標

本プロジェクトでは、以下の目標を掲げて開発に取り組みました。

  • IT 技術が多分野の社会的課題解決に貢献できることを実証し、共有する。
  • 日頃の学修成果を応用し、未知の分野へ挑戦する機会とする。
  • 開発したシステムを実際に体験してもらうことで、成果発表の経験を積む。
  • 観客にシステムを楽しんでもらい、イベント全体の満足度向上に貢献する。

プロジェクト体制

開発はアジャイル開発のスクラム手法を採用しました。

  • 開発期間: 61日(約2か月)
  • 開発人数: 生徒 7 名、監督教員 2 名、企業協力 2 名
  • 役職構成:
    • スクラムマスター: 筆者
    • プロダクトオーナー: 1 名
    • 開発チーム
      • フロントエンド: 筆者含む 2 名
        • デザイン: 1 名
        • 演出処理: 筆者
      • バックエンド: 3 名
        • WebSocket 処理: 1 名
        • MIDI 関連処理: 1 名
        • Sleep 関連処理: 1 名
      • インフラ: 1 名
    • 本番司会登壇者: 2 名
  • 監督教師・統括プロジェクトマネージャー: SIW の教員
  • 企業協力: オチュア株式会社様(https://www.otua.jp/

開発の経緯とタイムライン

企画立案期(2025 年 2 月初旬)

本番イベントである 2025 年 4 月 11 日の両国国技館での開催に向け、タイトなスケジュールで計画がスタートしました。

  • 2025 年 2 月 4 日: 初回定例会議にて、演出の基本方針を決定しました。

  • 2025 年 2 月 8 日: 第 2 回リハーサルを TSR で実施し、基本要件の確認を行いました。

    # リハーサルでの確認ポイント
    
    - 各専門学校の演出要望の収集
    - スマートフォンでの色表示やフラッシュ効果の確認
    - 「現地にネット回線があるかどうか」などの技術的制約の確認
    

技術選定期(2025 年 2 月中旬)

システムの実現に最適な技術スタックの選定を行いました。

  • 2025 年 2 月 10 日: 技術スタックの検討を開始しました。

    # 技術スタック
    
    言語:
    
    - TypeScript
    - JavaScript
    
    型付けのある言語がいい気がする
    
    6000 台近い通信に耐えるのかが気になる
    → スケーリングで対処?
    
    ランタイム:
    
    - Node.js
    
  • 2025 年 2 月 19 日: プロジェクト名とドメインが「LiveFx」に決定しました。

  • 当時は予定していましたが、最終的に使用しなかったスタックは以下の通りです。

    • DMX 照明値の送信
    • スマホ通知機能
  • 最終的な技術スタックは以下の通りです。

    • バックエンド: Node.js/Express + TypeScript
    • フロントエンド: React + TypeScript + Vite
    • リアルタイム通信: WebSocket (Socket.IO)
    • コンテナ技術: Docker + Kubernetes (AWS EKS)
    • 継続的インテグレーション: GitLab CI/CD

開発初期(2025 年 2 月下旬〜3 月上旬)

選定した技術スタックに基づき、本格的な開発に着手しました。

  • 2025 年 2 月 22 日: Docker 開発環境の共同構築セッションを実施しました。

  • 2025 年 2 月 25 日: 開発者オフラインミーティングにて実装戦略を決定しました。

  • WebSocket 通信の基本アーキテクチャ設計、Web MIDI API および Web Serial API を用いた外部デバイス制御機能の基礎実装を進めました。

  • チーム内の役割分担は以下のように決定しました。

    来週までの役割:
    
    - 〇〇: 照明
    - 〇〇: MIDI
    - 〇〇: websocket(front と)
    - 〇〇: websocket
    - 〇〇: front(フィルター)
    - 〇〇: front(フィルター)
    - 〇〇: sleep 関連
    

開発中期(2025 年 3 月中旬)

各機能の実装が進捗しました。

  • 2025 年 3 月 8 日: 開発定例会議にて、スマートフォン画面のスリープ防止機能の実装が報告されました。

  • NoSleep.tsxの導入:

    const NoSleepComponent = () => {
      useEffect(() => {
        let tapInterval: NodeJS.Timeout | null = null;
    
        // NoSleep.jsを非同期に読み込む
        loadNoSleep().then((NoSleepClass) => {
          const noSleepInstance = new NoSleepClass();
          // スリープ防止を有効にする
          const enableNoSleep = () => {
            noSleepInstance.enable();
            localStorage.setItem("noSleepEnabled", "true");
    
            // 10秒ごとにNoSleepを呼び出す
            if (!tapInterval) {
              tapInterval = setInterval(() => {
                enableNoSleep();
              }, 10000);
            }
          };
          // 最初のタップでNoSleepを有効化
          document.addEventListener("touchstart", enableNoSleepOnFirstTap, {
            once: true,
          });
        });
      }, []);
    
      return null;
    };
    
  • 演出パターンの実装も進められました。

    // 演出パターン定義
    const patternDict: {
      [key: string]: { patternName: string; groups: number[] };
    } = {
      "0": { patternName: "monoLight", groups: allGroups },
      "1": { patternName: "colorfulWave", groups: allGroups },
      "2": { patternName: "monoFlash", groups: allGroups },
      // 他多数のパターン...
    };
    
  • 2025 年 3 月 21 日: 第 3 回リハーサルを TSR で実施し、フィードバックを収集しました。

開発後期(2025 年 3 月下旬〜4 月上旬)

本番リリースに向けた最終調整段階に入りました。

  • 2025 年 3 月 29 日: MIDI 演出ボタンの配置が決定しました。

  • スタンバイ画面の実装:

    // StandbyScreen.tsx
    function StandbyScreen({ groupNumber }: Props) {
      // 学校ごとのメッセージ設定
      const schoolDict: { [key: string]: string } = {
        // 16校のデータ...
      };
    
      // 背景に桜を降らせるエフェクト
      return (
        <div className="zen-kurenaido">
          <CherryBlossom />
          {/* メインコンテンツ */}
          <div className="relative min-h-screen flex items-center justify-center overflow-hidden">
            {/* 背景部分 */}
            <div className="absolute inset-0 bg-cover bg-center" />
    
            {/* メインカード */}
            <div className="relative bg-white/90 backdrop-blur-sm p-8 rounded-lg shadow-xl">
              <div className="text-lg font-semibold text-gray-800">
                {schoolName}
              </div>
              <div className="text-2xl font-bold text-pink-600">
                ご入学おめでとうございます
              </div>
              {/* その他のメッセージ */}
            </div>
          </div>
        </div>
      );
    }
    

リハーサルと本番環境構築(2025 年 4 月)

本番稼働に向けたインフラ構築と最終リハーサルを行いました。

  • 2025 年 4 月 5 日: AWS 上に本番環境のインフラを構築しました。

    # CloudFormation によるインフラ構築手順
    
    1. ネットワークスタックのデプロイ
    2. セキュリティスタックのデプロイ
    3. モニタリングスタックのデプロイ
    4. EKS クラスタのデプロイ
    5. アプリケーションのデプロイ
    
  • 2025 年 4 月 10 日: 両国国技館にて前日リハーサルを実施しました。

  • ロードバランシングとオートスケーリングの最終調整を行いました。

  • モニタリング機能を導入しました。

    // MonitoringScreen.tsx
    const SchoolScreen = ({ name, url }: MonitoringScreenProps) => {
      return (
        <div className="flex flex-col items-center space-y-2">
          <div className="bg-slate-800 p-2 rounded-lg w-full">
            <h3 className="text-sm text-center text-white font-medium">{name}</h3>
          </div>
          <div className="relative bg-black rounded-lg border-2 border-slate-700 w-full aspect-[9/16]">
            <iframe
              src={url}
              className="absolute inset-0 w-full h-full"
              title={name}
            />
          </div>
        </div>
      );
    };
    

本番デプロイと実行(2025 年 4 月 11 日)

計画通り、本番イベントでのシステム運用を実施しました。

  • 2025 年 4 月 11 日: 本番イベント(両国国技館)
    • 開場: 12:00
    • 開式: 13:00
    • 閉式: 15:00
  • 専門学校 16 校の観客が一斉にスマートフォンで演出に参加しました。
  • 実際の会場でのシステム運用を完遂しました。

振り返りとドキュメント化(2025 年 4 月中旬)

プロジェクトの成果と課題を整理し、記録しました。

  • 2025 年 4 月 13 日: 「当日メモ」と「残務備忘録」を作成しました。
  • 2025 年 4 月 21 日: 最終振り返り会議を実施しました。

システムアーキテクチャ詳細

システム全体

system-overflow

AWS 構成図

aws

シーケンス図

sequence-diagram

フロントエンド構成

1. コントロール PC 用フロントエンド(オペレータ向け)

  • 技術スタック: React + TypeScript + Vite

  • 主要機能:

    • Web MIDI API: MIDI コントローラーとの連携

      // MIDIコントローラー操作の処理
      MIDIControl = new MIDIController(
        socket,
        patternDict,
        changeFilterDict,
        setError,
        sendPattern,
        changeFilter,
        buttonStatValue,
        changePage,
        changePageDict
      );
      
      MIDIControl.setMIDIMessageCallback((status, note) => {
        if (status === buttonStatValue) {
          handleCellClick(note);
        }
      });
      
    • Web Serial API: 照明機器制御

  • 画面構成:

    • MIDI ページ: MIDI と連携したコントローラー操作 UI

      midi-page

    • モニタリングページ: 各グループのスマホ画面確認

2. スマートフォン用フロントエンド(観客向け)

  • 技術スタック: React + TypeScript + Vite
  • 主要機能:
    • スリープ防止機能(NoSleep.tsx)
  • 画面遷移:
    • ロゴ画面: 最初に表示されるロゴ画面
    • 待機画面: 学校別メッセージ表示
    • 演出画面: リアルタイムエフェクト表示
    • エピローグ画面: 桜吹雪などの演出

バックエンド構成

1. コントロール PC 用バックエンド

  • 技術スタック: Node.js + Express + TypeScript
  • 主要機能:
    • TCP/IP 通信によるスマホバックエンドとの連携
    • MIDI 信号の解析と演出指示の生成

2. スマートフォン用バックエンド

  • 技術スタック: Node.js + Express + TypeScript

  • 主要機能:

    • WebSocket (Socket.IO) サーバー

    • 16 グループのネームスペース管理

      // 16グループのネームスペース設定
      const smartPhoneNamespaceList = Array.from({ length: 16 }, (_, i) => {
        const smartPhoneTag = `smart-phone-${(i + 1)
          .toString()
          .padStart(2, "0")}`;
        const namespace = io.of(
          `/ws/smart-phone/${(i + 1).toString().padStart(2, "0")}`
        );
        namespace.on("connection", (socket: Socket) => {
          // メトリクスのカウンタをインクリメント
          wsTotalConnectionGauge.inc({ namespace: smartPhoneTag });
          wsConnectionCounter.inc({ namespace: smartPhoneTag });
          // 初期データを送信
          namespace.to(socket.id).emit("currentState", initialData);
        });
        return namespace;
      });
      

インフラストラクチャ

  • コンテナオーケストレーション: AWS EKS (Kubernetes) クラスター

    • 本番環境: t3.medium インスタンス、3-6 ノード
    • オートスケーリング: CPU 60%、メモリ 70%で拡張
  • ロードバランシング: AWS ALB

  • イメージレジストリ: AWS ECR

  • DNS 管理: Route53 (livefx.siw.ac.jp)

  • CI/CD: GitLab CI/CD パイプライン

    # .gitlab-ci.yml
    build_front_smartphone:
      stage: build
      script:
        - cd front-smartphone || exit 1
        - docker build --build-arg NODE_ENV=${NODE_ENV} -t ${ECR_REGISTRY}/front-smartphone:${CI_COMMIT_SHA} .
        - docker tag ${ECR_REGISTRY}/front-smartphone:${CI_COMMIT_SHA} ${ECR_REGISTRY}/front-smartphone:latest
        - docker push ${ECR_REGISTRY}/front-smartphone:${CI_COMMIT_SHA}
        - docker push ${ECR_REGISTRY}/front-smartphone:latest
    

モニタリングシステム

プロジェクトの重要な要素として、大規模な同時接続を安定して処理するためのモニタリングシステムを構築しました。

Prometheus と Grafana による監視体制

  • 監視インフラ:

    • Prometheus: メトリクス収集とアラート管理
    • Grafana: リアルタイムダッシュボードとデータ可視化
    • Kubernetes カスタムメトリクス
  • 主要監視項目:

    • WebSocket 接続数(リアルタイム/累計)
    • Pod リソース使用率(CPU/メモリ)
    • レスポンスタイム・レイテンシ
    • エラー率とリクエスト成功率

以下は、WebSocket 接続数を計測するためのカスタムメトリクス実装例です:

// metrics.ts
import promClient from "prom-client";

// WebSocket総接続数のゲージメトリクス
export const wsTotalConnectionGauge = new promClient.Gauge({
  name: "websocket_connections_total",
  help: "現在のWebSocket接続数",
  labelNames: ["namespace"],
});

// WebSocket接続カウンター
export const wsConnectionCounter = new promClient.Counter({
  name: "websocket_connections_created_total",
  help: "WebSocket接続の累計数",
  labelNames: ["namespace"],
});

// WebSocket切断カウンター
export const wsDisConnectionCounter = new promClient.Counter({
  name: "websocket_connections_closed_total",
  help: "WebSocket切断の累計数",
  labelNames: ["namespace"],
});

// メトリクスエンドポイント用の関数
export async function getMetrics() {
  return await promClient.register.metrics();
}

これらのメトリクスはバックエンドの /metrics エンドポイントを通じて公開され、Prometheus によって定期的に収集されます:

// app.ts
app.get(
  "/metrics",
  cors({
    origin: "*",
    credentials: false,
  }),
  async (req: Request, res: Response) => {
    try {
      const metrics: string = await getMetrics();
      res.set("Content-Type", "text/plain");
      res.send(metrics);
    } catch (err) {
      res.status(500).send("Error collecting metrics");
    }
  }
);

Grafana ダッシュボード

Grafana では、以下のカスタムダッシュボードを作成し、リアルタイムでシステムの状態を監視しました:

  • WebSocket 接続ダッシュボード: グループ別のリアルタイム接続数と時系列推移
  • システムリソースダッシュボード: Pod 別の CPU/メモリ使用率とネットワーク I/O

特に重要視したのは、WebSocket の接続状態モニタリングです。リハーサルと本番では、オペレーションチーム用の専用モニターを設置し、常にシステム状態を可視化することで、問題の早期発見と対応を可能にしました。

grafana-dashboard

直面した技術的課題と解決策

開発過程において、いくつかの技術的課題に直面しましたが、チームで協力し解決に至りました。

1. 大規模同時接続への対応

  • 課題: 最大 6,000 台のスマートフォンからの同時接続処理。
  • 解決策:
    • Kubernetes (AWS EKS) によるオートスケーリングの実装。
    • WebSocket コネクションプールの最適化。
    • キャッシュ戦略の導入によるレスポンス性能の向上。
    • Prometheus によるリアルタイム接続数監視とアラート設定

2. スマートフォンのスリープ防止

  • 課題: イベント中の長時間にわたるスマートフォン画面のスリープモード移行の阻止。
  • 解決策:
    • NoSleep.js ライブラリの導入。
    • 定期的な再確認ロジックの実装。
    • ユーザータッチイベントをトリガーとしたスリープ防止機能の有効化

3. タイミング同期の精度

  • 課題: 多数のクライアントデバイス間における演出タイミングの同期。

  • 解決策:

    • クライアント側での時刻基準による演出タイミング調整。
    • サーバー側での送信時刻(UNIX 時間ミリ秒)を付与したデータ配信。
    • レイテンシモニタリングによる同期ずれの可視化
    // 送信時のデータ形式
    const data: tcpJSONData = {
      messageType: string,
      data: string | number,
      groups: number[],
      sendWhenUNIXTimeMs: number, // 送信時刻
      runUNIXTimeMs: number,      // 実行時刻
    };
    

4. 接続の安定性

  • 課題: 会場内の無線 LAN 環境における接続安定性の確保。
  • 解決策:
    • 再接続メカニズムの実装。
    • バックオフ戦略の導入によるサーバー負荷軽減。
    • フェールセーフモードの導入による冗長性の確保。
    • エラー率とリトライ回数のメトリクス化と閾値アラート設定

数字での評価

プロジェクトの規模と成果を定量的に示します。

使用ツール

  • GitLab イシュー数: 116 件
  • GitLab マージリクエスト数: 118 件

インフラ

  • WebSocket 最大同時接続数: 3000 以上
  • WebSocket 累計接続数: 13354
  • ELB Pod 数: 28

トラブルシューティング事例

開発中に発生した主要なトラブルとその対応について報告します。

1. TypeScript 型エラー事件 (2025/03/21)

第 3 回リハーサル@TSR で大きなトラブルが発生しました。このリハーサルでは、他校の演出担当者に実際にシステムを触ってもらい、本番での演出プランを固める重要な機会でした。

当日朝、会場に到着して現地のネットワークでシステムを立ち上げようとしたところ、フロントエンドの Docker コンテナが起動せず、以下のような TypeScript の型エラーが表示されました:

ERROR in src/components/PerformanceScreen.tsx:142:43
TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
  Type 'undefined' is not assignable to type 'string'.

調査したところ、前日に急いでマージした機能追加ブランチで、TypeScript の型定義が不適切なままコミットされており、ビルド時に型チェックでエラーが発生していました。ローカルで個別に開発していた際はtsc --noEmitによる厳密な型チェックを行わず、動作確認のみで進めていたことが原因でした。

急遽、前バージョンの安定ブランチにロールバックして対応しましたが、新機能(スマホ振動パターンや演出切替画面)がない状態での説明となってしまい、他校の担当者にシステムの完成度について不安を与えてしまいました。

対応策と教訓

  • コミット前の型チェックルールを導入
  • CI/CD パイプラインに型チェックステージを追加:ブランチマージ前に自動検証
  • デプロイ前の統合テスト環境を構築:本番同等環境での事前検証
  • チェックリスト作成:リハーサル前に確認すべき項目を明文化
  • ビルドプロセスの Prometheus メトリクスを追加:コンパイルエラー検知の早期化

このトラブルをきっかけに、「動くコードよりも型安全なコード」という意識が全チームに浸透し、その後の開発品質が大幅に向上しました。

2. GitLab Temp User 事件 (2025/03/25)

型エラーを修正した 8-docker-file ブランチとコントロール PC 画面の機能追加をした 41-control-pc ブランチをマージする際に奇妙な事件が発生しました。

両ブランチはともに重要ファイルに変更を加えており、予想通りコンフリクトが検出されました。会議中だったため、チームメンバーと一緒にコンフリクト解決する予定でした。しかし、マージリクエスト画面を再度確認した際、GitLab が「コンフリクトはありません」と表示していました。

不思議に思いながらもマージを進め、その夜、突然「dev ブランチのアプリケーションが起動しない」という緊急連絡が入りました。

GitLab のコミット履歴を確認すると、コンフリクト解決者として「Temp User」という見知らぬアカウントが記録されていました。これは GitLab の自動マージ機能が何らかの理由で作動し、コンフリクトを機械的に「解決」してしまったと考えられます。

temp-user

緊急対応と再発防止策

  1. 深夜の緊急修正:両方の設定を正しく統合した修正パッチをリリース

  2. GitLab のプロジェクト設定変更:

    • 「Auto-merge」機能の無効化
    • 「Squash commits」オプションの無効化
    • コンフリクト解決時の必須レビューワー設定
  3. マージプロセスの厳格化:

    • マージ前後の動作確認を義務付けるチェックリスト導入
    • コンフリクト解決時の必ずペアレビュー実施
  4. デプロイ後の自動ヘルスチェック導入:

    • Prometheus メトリクスを用いたアプリケーション起動状態の監視
    • CI/CD パイプラインへのヘルスチェックステージ追加

この事件を教訓に、「自動化ツールを過信しない」「マージ後は必ず動作確認」という原則が徹底され、その後のデプロイトラブルは大幅に減少しました。

実際の開発現場

dev

開発は、定期的なオンライン定例会議と、重要な開発マイルストーンにおけるオフライン会議を組み合わせて推進しました。 情報共有には Notion を、コード管理には GitLab を活用し、分散開発体制下での円滑な連携を図りました。

チームは機能別に編成され、フロントエンド・バックエンド・インフラの各担当が並行して開発を進めました。 リハーサルごとに実装優先度を設定し、段階的にシステムの完成度を高める戦略を採用しました。

感想

技術的な学び

  • クラウドネイティブなアーキテクチャ設計とスケーリング手法の習得。
  • WebSocket 通信を用いた大規模リアルタイムシステムの構築経験。
  • CI/CD パイプライン導入による開発フローの効率化。
  • スマートフォンブラウザ特有の制約(スリープ防止等)への対応策。

プロジェクト管理の学び

  • 明確なマイルストーン設定とタイムライン管理の重要性の認識。
  • 実環境を想定した段階的リハーサルの有効性の確認。
  • コミュニケーションツール(Notion, GitLab)の活用によるチーム連携の円滑化。
  • チーム内の課題解決、協力企業やクライアントとの密接な連携の重要性。

ビジネス的成果

  • 16 校の専門学校生が参加する大規模イベントでのシステム提供成功。
  • 観客のスマートフォンを活用した新たな演出体験の創出。
  • 将来的な拡張性を有するプラットフォームの基盤構築。

おわりに

LiveFx プロジェクトは、短期間で構想から実装、本番運用までを完遂した、技術的に挑戦的なプロジェクトでした。
最新の Web 技術とクラウドインフラを組み合わせることで、数千人規模のインタラクティブな演出を可能にし、イベントの体験価値を高めることに貢献できたと考えます。

本プロジェクトを通じて得た経験とノウハウは、今後の Web 開発やクラウドシステム構築において貴重な財産となります。
特に、リアルタイム性とスケーラビリティの両立、モバイルデバイスの特性を考慮した実装、そして大規模イベントにおける本番運用の経験は、エンジニアとしての成長において非常に価値の高いものでした。

Discussion