League Of Legendsの配信用オーバーレイを作る【JavaScript編】

分間CSオーバーレイ LeagueOfLegends
分間CSオーバーレイ

 League Of LegendsのオーバーレイをJavaScriptで作れたらいいよね!という話です。

 LoLの世界大会であるWorldsやMSIのようなスコアボードやトップバーを作っていましたが、Python+OBSで作っていたため、シーンアイテム数が膨大になり、レイアウトの修正が難しくなっていました。
 Pythonでゴリゴリで作っていたので「配布しても誰も修正できないじゃん・・・?」という漠然とした思いがありました。

 「OBSってブラウザソースが表示できるじゃん?」と思い出し、JavaScript+HTMLでオーバーレイを作れれば、配布も改変も容易にできるのでは!という思い付きです。

JavaScriptでの利点

 JavaScriptで作成する利点です。

  • ブラウザで動くため、開発環境構築が不要
  • レイアウトの修正もHTMLなら容易
  • OBSの「ブラウザソース」で簡単に動く

 配布が簡単・誰でも実行可能・誰でも修正可能というのがJavaScriptの利点です。

お試し

 お試しとしてJavaScriptで分間CSを表示するJavaScriptを作ってみます。

 JavaScriptのfetchでLiveClientDataAPIからCreepScoreを取得し、分間何CSとれたか表示するオーバーレイです。

お試しソース

 結論から言うと、「LiveClientDataAPIがブラウザからの呼び出しを許容していない」ため、JavaScript単体での実現はできませんでした。

 以下がお試しソースと発生したエラーです。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>LoL 分間CSモニター</title>

  <style>
    body {
      margin: 0;
      padding: 0;
      background: #0b0b10;
      color: #f5f5f5;
      font-family: system-ui, sans-serif;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
    }

    .cs-container {
      background: #15151f;
      border: 1px solid #2f2f40;
      border-radius: 12px;
      padding: 24px 32px;
      text-align: center;
      min-width: 260px;
    }

    .cs-title {
      margin: 0 0 12px;
      font-size: 20px;
      color: #c8c8ff;
    }

    .cs-value {
      font-size: 56px;
      font-weight: 700;
      color: #ffdf6b;
      margin-bottom: 12px;
    }

    .cs-sub {
      display: flex;
      justify-content: space-between;
      font-size: 14px;
      color: #b0b0c8;
      margin-bottom: 10px;
      gap: 16px;
    }

    .cs-status {
      font-size: 12px;
      color: #8888aa;
    }
  </style>
</head>

<body>
  <div class="cs-container">
    <h1 class="cs-title">分間CS</h1>
    <div class="cs-value" id="csPerMinute">--.-</div>

    <div class="cs-sub">
      <span>現在CS: <span id="currentCs">-</span></span>
      <span>経過時間: <span id="gameTime">-</span></span>
    </div>

    <div class="cs-status" id="statusMessage">Live Client API 待機中...</div>
  </div>

  <script>
    const PROXY_URL = "https://127.0.0.1:2999/liveclientdata/allgamedata";

    const csPerMinuteEl = document.getElementById("csPerMinute");
    const currentCsEl = document.getElementById("currentCs");
    const gameTimeEl = document.getElementById("gameTime");
    const statusMessageEl = document.getElementById("statusMessage");

    async function fetchJson(url) {
      const res = await fetch(url);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return await res.json();
    }

    function formatTime(seconds) {
      const m = Math.floor(seconds / 60);
      const s = Math.floor(seconds % 60);
      return `${m}:${s.toString().padStart(2, "0")}`;
    }

    async function updateCsPerMinute() {
      try {
        statusMessageEl.textContent = "更新中...";

        // Flask のレスポンスは { ok: true, data: {...} }
        const api = await fetchJson(PROXY_URL);

        if (!api.ok) {
          statusMessageEl.textContent = "Live Client API に接続できません";
          return;
        }

        const data = api.data; // ← 本物の Live Client API の JSON

        // Riot ID でプレイヤー特定
        const activeRiotId = data.activePlayer.riotId;
        const me = data.allPlayers.find(p => p.riotId === activeRiotId);

        if (!me) {
          statusMessageEl.textContent = "プレイヤー情報が見つかりません";
          return;
        }

        const cs = me.scores.creepScore;
        const gameTime = data.gameData.gameTime;

        const minutes = gameTime / 60;
        const csPerMinute = minutes > 0 ? cs / minutes : 0;

        currentCsEl.textContent = cs;
        gameTimeEl.textContent = formatTime(gameTime);
        csPerMinuteEl.textContent = csPerMinute.toFixed(1);

        statusMessageEl.textContent = "更新完了";
      } catch (err) {
        console.error(err);
        statusMessageEl.textContent = "Live Client API に接続できません";
        csPerMinuteEl.textContent = "--.-";
      }
    }

    updateCsPerMinute();
    setInterval(updateCsPerMinute, 10000);
  </script>
</body>
</html>

Access to fetch at ‘https://127.0.0.1:2999/liveclientdata/allgamedata’ from origin ‘null’ has been blocked by CORS policy: The ‘Access-Control-Allow-Origin’ header has a value ‘https://127.0.0.1:2999’ that is not equal to the supplied origin. Have the server send the header with a valid value.

 ブラウザにはCORSというCross-Origin Resource Sharing(クロスオリジン・リソース・シェアリング)という仕組みがあります。CORSは異なるオリジン(≒アドレス)へのアクセスを阻止する仕組みです。

 LiveClientDataAPIはJavaScriptからのアクセスを想定していないため、CORSヘッダーを返さず、CORSエラーが発生します。

CORSエラー対策

 「LiveClientDataAPI は CORS ヘッダーを返さないため、ブラウザから直接アクセスすると CORS エラーが発生します。
 この問題を解決するために、「LiveClientDataAPI のデータを取得し、必要な CORS ヘッダーを付与して返す “CORS 対応 API プロキシ”」 を 作成します。

 プロキシを作る、というとハードルが高く感じますが、とても簡単な作りとなっています。
Copilotには「Node.jsがオススメ」と言われましたが、まずは形にするため慣れているPythonで作成します。

 やっていることはとても簡単です。
  ①LiveClientDataAPIにアクセス
  ②CORSヘッダーを付与する
 の2点です。

 以下「CORS 対応 API プロキシ」を実行することでJavaScriptからLiveClientDataAPIにアクセスすることができるようになります。

from flask import Flask, jsonify
import requests
import urllib3

# 証明書の警告を無効化
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

app = Flask(__name__)
session = requests.Session()

# LoLクライアントのローカルAPIエンドポイント
BASE            = "https://127.0.0.1:2999/liveclientdata"

# LoLクライアントからデータを取得する関数
def fetch_lcu(path):
    try:
        r = session.get(f"{BASE}/{path}", verify=False, timeout=0.3)
        return {"ok": True, "data": r.json()}
    except Exception as e:
        return {"ok": False, "error": str(e)}

# CORS対策のため、レスポンスにヘッダーを追加
@app.after_request
def add_cors_headers(response):
    response.headers["Access-Control-Allow-Origin"] = "*"
    return response

# LoLクライアントから全ゲームデータを取得するエンドポイント
@app.route("/allgamedata")
def allgamedata():
    return jsonify(fetch_lcu("allgamedata"))

if __name__ == "__main__":
    app.run(port=3000, debug=True)

完成版

 お試しソースのURLを変更します。

 JavaScript側のURLを以下のように「CORS 対応 API プロキシ」に変更します。

const PROXY_URL = "https://127.0.0.1:2999/liveclientdata/allgamedata";
const PROXY_URL = "http://localhost:3000/allgamedata";

完成版ソース全量

 重複しますが見やすくするために分間CS表示JavaScriptとCORS対応APIプロキシのソース全量をまとめます。

分間CS表示JavaScript

 分間CS表示JavaScriptのソース全量です。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>LoL 分間CSモニター</title>

  <style>
    body {
      margin: 0;
      padding: 0;
      background: #0b0b10;
      color: #f5f5f5;
      font-family: system-ui, sans-serif;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
    }

    .cs-container {
      background: #15151f;
      border: 1px solid #2f2f40;
      border-radius: 12px;
      padding: 24px 32px;
      text-align: center;
      min-width: 260px;
    }

    .cs-title {
      margin: 0 0 12px;
      font-size: 20px;
      color: #c8c8ff;
    }

    .cs-value {
      font-size: 56px;
      font-weight: 700;
      color: #ffdf6b;
      margin-bottom: 12px;
    }

    .cs-sub {
      display: flex;
      justify-content: space-between;
      font-size: 14px;
      color: #b0b0c8;
      margin-bottom: 10px;
      gap: 16px;
    }

    .cs-status {
      font-size: 12px;
      color: #8888aa;
    }
  </style>
</head>

<body>
  <div class="cs-container">
    <h1 class="cs-title">分間CS</h1>
    <div class="cs-value" id="csPerMinute">--.-</div>

    <div class="cs-sub">
      <span>現在CS: <span id="currentCs">-</span></span>
      <span>経過時間: <span id="gameTime">-</span></span>
    </div>

    <div class="cs-status" id="statusMessage">Live Client API 待機中...</div>
  </div>

  <script>
    const PROXY_URL = "http://localhost:3000/allgamedata";

    const csPerMinuteEl = document.getElementById("csPerMinute");
    const currentCsEl = document.getElementById("currentCs");
    const gameTimeEl = document.getElementById("gameTime");
    const statusMessageEl = document.getElementById("statusMessage");

    async function fetchJson(url) {
      const res = await fetch(url);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return await res.json();
    }

    function formatTime(seconds) {
      const m = Math.floor(seconds / 60);
      const s = Math.floor(seconds % 60);
      return `${m}:${s.toString().padStart(2, "0")}`;
    }

    async function updateCsPerMinute() {
      try {
        statusMessageEl.textContent = "更新中...";

        // Flask のレスポンスは { ok: true, data: {...} }
        const api = await fetchJson(PROXY_URL);

        if (!api.ok) {
          statusMessageEl.textContent = "Live Client API に接続できません";
          return;
        }

        const data = api.data; // ← 本物の Live Client API の JSON

        // Riot ID でプレイヤー特定
        const activeRiotId = data.activePlayer.riotId;
        const me = data.allPlayers.find(p => p.riotId === activeRiotId);

        if (!me) {
          statusMessageEl.textContent = "プレイヤー情報が見つかりません";
          return;
        }

        const cs = me.scores.creepScore;
        const gameTime = data.gameData.gameTime;

        const minutes = gameTime / 60;
        const csPerMinute = minutes > 0 ? cs / minutes : 0;

        currentCsEl.textContent = cs;
        gameTimeEl.textContent = formatTime(gameTime);
        csPerMinuteEl.textContent = csPerMinute.toFixed(1);

        statusMessageEl.textContent = "更新完了";
      } catch (err) {
        console.error(err);
        statusMessageEl.textContent = "Live Client API に接続できません";
        csPerMinuteEl.textContent = "--.-";
      }
    }

    updateCsPerMinute();
    setInterval(updateCsPerMinute, 10000);
  </script>
</body>
</html>

CORS対応APIプロキシ

 CORS対応APIプロキシのソース全量です。

from flask import Flask, jsonify
import requests
import urllib3

# 証明書の警告を無効化
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

app = Flask(__name__)
session = requests.Session()

# LoLクライアントのローカルAPIエンドポイント
BASE            = "https://127.0.0.1:2999/liveclientdata"

# LoLクライアントからデータを取得する関数
def fetch_lcu(path):
    try:
        r = session.get(f"{BASE}/{path}", verify=False, timeout=0.3)
        return {"ok": True, "data": r.json()}
    except Exception as e:
        return {"ok": False, "error": str(e)}

# CORS対策のため、レスポンスにヘッダーを追加
@app.after_request
def add_cors_headers(response):
    response.headers["Access-Control-Allow-Origin"] = "*"
    return response

# LoLクライアントから全ゲームデータを取得するエンドポイント
@app.route("/allgamedata")
def allgamedata():
    return jsonify(fetch_lcu("allgamedata"))

if __name__ == "__main__":
    app.run(port=3000, debug=True)

実行イメージ

 分間CSJavaScriptの実行イメージは以下の通りです。

分間CSオーバーレイ

 作成したJavaScriptをOBSのブラウザソースで取り込むことで配信画面に分間CSを表示することができます。オーバーレイのデザインはCSSで変更することができます。

今後

 JavaScriptによるLeague Of Legendsのオーバーレイ作成について目途がつきました。

 今後はスコアボードやトップバーの作成、Node.jsによる「CORS対応プロキシ安定板」の作成を進めていいこうと思います。

コメント