Greasy Fork 支持简体中文。

SLIVE - 외부장치 방송 OBS 연동 제어

외부장치 방송 대시보드에 OBS 방송 시작 및 종료, 대기 후 종료 버튼을 추가합니다.

// ==UserScript==
// @name         SLIVE - 외부장치 방송 OBS 연동 제어
// @namespace    https://www.afreecatv.com/
// @version      2.0.1
// @description  외부장치 방송 대시보드에 OBS 방송 시작 및 종료, 대기 후 종료 버튼을 추가합니다.
// @author       Jebibot
// @match        https://dashboard.sooplive.co.kr/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=www.sooplive.co.kr
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  let obs;

  const broadcastInfo = document.querySelector(".broadcast_info");
  if (broadcastInfo == null) {
    return;
  }

  GM_addStyle(`
    .obs_status {
      font-size: 16px;
    }

    .obs_status::before {
      content: '';
      display: inline-block;
      width: 8px;
      height: 8px;
      margin: 0 4px;
      border-radius: 50%;
      background-color: gray;
    }

    .obs_status.error::before {
      background-color: red;
    }

    .obs_status.connected::before {
      background-color: green;
    }

    .obs_controls {
      display: flex;
      flex-direction: row;
      gap: 10px;
    }

    .obs_controls button {
      display: block;
      background: #f7f7f7;
      flex: 1;
      height: 40px;
      line-height: 40px;
      color: #333;
      font-size: 16px;
      font-weight: normal
    }

    .obs_controls button:disabled {
      background: #e3e3e3;
      color: #888;
    }

    body.thema_dark .obs_controls button {
      background: #2e2e2f;
      color: #ccc
    }

    body.thema_dark .obs_controls button:disabled {
      background: #3f3f46;
      color: #888;
    }
    `);

  GM_registerMenuCommand("== 도구 > WebSocket 서버 설정 > 서버 정보 표시 ==");

  let address = GM_getValue("address", "127.0.0.1");
  const promptAddress = () => {
    address = prompt("서버 IP", address);
    if (!address) {
      return;
    }
    GM_setValue("address", address);
    GM_registerMenuCommand(`서버 IP: ${address}`, promptAddress, {
      id: addressMenu,
    });
    reconnect();
  };
  const addressMenu = GM_registerMenuCommand(
    `서버 IP: ${address}`,
    promptAddress
  );

  let port = GM_getValue("port", "4455");
  const promptPort = () => {
    const newPort = prompt("서버 포트", port);
    if (!newPort || isNaN(newPort)) {
      alert("유효한 포트를 입력해주세요");
      return;
    }
    port = newPort;
    GM_setValue("port", port);
    GM_registerMenuCommand(`서버 포트: ${port}`, promptPort, { id: portMenu });
    reconnect();
  };
  const portMenu = GM_registerMenuCommand(`서버 포트: ${port}`, promptPort);

  let password = GM_getValue("password");
  GM_registerMenuCommand(`서버 비밀번호`, () => {
    password = prompt("서버 비밀번호 (방송 노출 주의)", password);
    if (!password) {
      return;
    }
    GM_setValue("password", password);
    reconnect();
  });

  const control = document.createElement("div");
  control.classList.add("broadcast_info");
  broadcastInfo.insertAdjacentElement("beforebegin", control);

  const title = document.createElement("h5");
  title.textContent = "OBS 제어";
  control.appendChild(title);

  const status = document.createElement("span");
  status.classList.add("obs_status");
  status.textContent = "연결되지 않음";
  title.appendChild(status);

  const content = document.createElement("div");
  content.classList.add("broadcast_info_content", "obs_controls");
  control.appendChild(content);

  const onClick = async (e) => {
    if (obs == null) {
      return;
    }
    const frm = document.getElementById("dashboardFrm");
    if (frm == null) {
      return;
    }
    const shouldWait = e.target.textContent !== "방송 종료";
    const shouldWaitYN = shouldWait ? "Y" : "N";
    if (frm.is_wait.value !== shouldWaitYN) {
      frm.is_wait.value = shouldWaitYN;
      frm.frmWait.checked = shouldWait;
      const check = document.querySelector('label[for="check4"]');
      if (shouldWait) {
        check?.classList.add("on");
      } else {
        check?.classList.remove("on");
      }

      try {
        const res = await fetch(
          "https://dashboard.sooplive.co.kr/api/app_dashboard.php",
          { method: "POST", body: new FormData(frm) }
        );
        const result = await res.json();
        if (!result.channel.remsg.includes("정상적")) {
          throw new Error(result.channel.remsg);
        }
      } catch (e) {
        alert(e.message);
        return;
      }
    }
    await obs.call(
      e.target.textContent === "방송 시작" ? "StartStream" : "StopStream"
    );
  };

  const streamButton = document.createElement("button");
  streamButton.disabled = true;
  streamButton.textContent = "방송 시작";
  streamButton.addEventListener("click", onClick);
  content.appendChild(streamButton);

  const standbyButton = document.createElement("button");
  standbyButton.style.display = "none";
  standbyButton.textContent = "대기 후 종료";
  standbyButton.addEventListener("click", onClick);
  content.appendChild(standbyButton);

  const setStatus = (statusText, error) => {
    status.textContent = statusText;
    if (error) {
      status.classList.remove("connected");
      status.classList.add("error");
      streamButton.disabled = true;
      standbyButton.disabled = true;
    } else {
      status.classList.remove("error");
      status.classList.add("connected");
      streamButton.disabled = false;
      standbyButton.disabled = false;
    }
  };

  const setState = (state) => {
    switch (state) {
      case "OBS_WEBSOCKET_OUTPUT_STARTING":
        streamButton.disabled = true;
        streamButton.textContent = "연결 중...";
        break;
      case "OBS_WEBSOCKET_OUTPUT_STARTED":
        streamButton.disabled = false;
        standbyButton.style.display = "";
        streamButton.textContent = "방송 종료";
        break;
      case "OBS_WEBSOCKET_OUTPUT_STOPPING":
        streamButton.disabled = true;
        standbyButton.style.display = "none";
        streamButton.textContent = "방송 중단 중...";
        break;
      case "OBS_WEBSOCKET_OUTPUT_STOPPED":
        streamButton.disabled = false;
        standbyButton.style.display = "none";
        streamButton.textContent = "방송 시작";
        break;
    }
  };

  let retry = 0;
  const connect = async () => {
    try {
      obs = new OBSWebSocket();
      obs.on("ConnectionClosed", () => {
        setStatus(`연결 끊김`, true);
        setTimeout(connect, Math.min(100 * Math.pow(2, retry++), 3000));
      });
      obs.on("StreamStateChanged", ({ outputState }) => {
        setState(outputState);
      });
      await obs.connect(`ws://${address}:${port}`, password, {
        eventSubscriptions: OBSWebSocket.EventSubscription.Outputs,
      });
      setStatus(`연결됨`);
      retry = 0;

      const { outputActive } = await obs.call("GetStreamStatus");
      setState(
        outputActive
          ? "OBS_WEBSOCKET_OUTPUT_STARTED"
          : "OBS_WEBSOCKET_OUTPUT_STOPPED"
      );
    } catch (e) {
      console.error(e);
      setStatus(`오류`, true);
    }
  };

  const reconnect = async () => {
    if (obs == null) {
      return;
    }
    try {
      await obs.disconnect();
    } catch {}
    return connect();
  };

  const s = document.createElement("script");
  s.type = "text/javascript";
  s.src =
    "https://cdn.jsdelivr.net/npm/[email protected]/dist/obs-ws.global.js";
  s.onload = connect;
  document.body.appendChild(s);
})();