OBS 웹소켓 연결하는 법

철이·2025년 2월 10일

개발

목록 보기
3/3

스트리머 필수템, OBS

OBS는 무엇을 줄인 말일까요?
바로 Open Broadcaster Software의 약자입니다.
방송을 편하게 해주는 프로그램이라는 뜻입니다.

회사에서 OBS 방송 기능을 도입하는 프로젝트를 개발하게 되어서 처음 알게된 프로그램인데요, 네이버 치지직이나 유튜브에서 방송하려면 필수로 사용하는 화면 녹화 어플리케이션이었습니다.


OBS 방송 생성 플로우

1. 방송 생성 진입점에서 'OBS 라이브 생성' 버튼 클릭
2. 고유 스트림키 조회
3. OBS 스튜디오 설정창에서 스트림 주소 URL과 스트림키 복붙
3. 방송 생성 POST 요청 시 OBS 세션 송출 API 호출
4. OBS 스튜디오에서 '방송 시작' 버튼 클릭

이번에 작업한 OBS 방송 기능의 플로우는 위와 같습니다.
방송을 송출할 스트림 주소와 유저의 고유 스트림키를 설정해주면 방송 시작 버튼을 눌러서 라방을 시작할 수 있습니다.

유저 반응을 검증해보는 Beta 기능이라서 방송 생성/송출만 MVP로 개발했습니다. 그런데 방송 송출을 OBS 스튜디오에 완전히 위임하다보니, 라이브 DJ가 OBS에서 어떤 인터랙션을 하는지 알 수 있는 방법이 없었습니다.

도와줘, OBS 웹소켓

이는 OBS 웹소켓을 연결하면 해결할 수 있습니다. v28 버전 이상을 다운로드 했다면 이미 프로그램 안에 웹소켓이 내장되어 있어서 따로 웹소켓 패키지를 다운로드할 필요가 없습니다. v27 이하 버전의 OBS를 사용한다면 공식 사이트에서 웹소켓을 다운로드 받을 수 있습니다.

OBS 스튜디오 > 도구 > 설정 > WebSocket 서버 설정

이렇게 웹소켓 설정을 켜주어야 테스트가 가능합니다.

이제 obs-websocket 5.x.x Protocol 문서를 참고하여 웹소켓을 연결해봅시다.

OBS 웹소켓 연결 플로우

1. 웹소켓 연결
2. 웹소켓이 Hello 메시지를 보냄
	2-1. 메시지에 authentication 필드가 있으면 인증 필요 O
    2-2. 메시지에 authentication 필드가 없으면 인증 필요 X
3. 인증 성공 시 Identify 과정에서 구독한 이벤트 수신 가능

코드와 함께 자세하게 작성해보겠습니다.

1. 웹소켓 연결

  useEffect(() => {
    const liveOnair = liveId > -1;
    if (liveOnair) {
      ws.current = new WebSocket(SOCKET_URL);

      ws.current.onopen = () => {
        console.log('<<< onopen');
      };

      ws.current.onmessage = (event) => handleMessage(event);

      ws.current.onerror = () => console.error('<<< onerror');
      ws.current.onclose = () => console.log('<<< onclose');
    }

    return () => {
      if (ws.current && ws.current.readyState === WebSocket.OPEN) {
        ws.current.close();
      }
    };
  }, [handleMessage, liveId, requestAuth]);

먼저 웹소켓을 연결합니다.

2. 인증

💡Hello (OpCode 0)

  • Sent from: obs-websocket
  • Sent to: Freshly connected websocket client
  • Description: First message sent from the server immediately on client connection. Contains authentication information if auth is required. Also contains RPC version for version negotiation.

웹소켓 연결에 성공하면 웹소켓이 인사를 합니다.
이때 authentication 필드를 포함하고 있다면 인증을 진행해야 합니다.

Hello 응답 메시지는 다음과 같은 형태입니다.

{
  "op": 0,
  "d": {
    "obsWebSocketVersion": "5.1.0",
    "rpcVersion": 1,
    "authentication": {
      "challenge": "+IxH4CnCiqpX1rM9scsNynZzbOe4KhDeYcTNS3PDaeY=",
      "salt": "lM1GncleQOaCu9lT1yeUZhFYnqhsLLP1G5lAGo3ixaI="
    }
  }
}

authentication 객체의 challengesalt는 클라이언트 인증 과정에 필요한 값입니다.
OBS WebSocket은 인증이 필요한 경우, 클라이언트가 서버에 직접 비밀번호를 전송하지 않도록 보안 메커니즘을 적용합니다.
이를 위해 Challenge-Response 인증 방식을 사용합니다.

salt: 서버에서 생성하는 랜덤한 문자열
이 값은 비밀번호를 안전하게 해싱하기 위한 값으로 사용됩니다.
클라이언트는 저장된 비밀번호와 이 salt 값을 결합하여 해시를 생성합니다.

challenge: 서버에서 제공하는 난수(Nonce)로, 클라이언트가 인증할 때 사용할 해싱의 입력값입니다.
클라이언트는 challenge를 이용해 최종적인 인증 문자열을 계산합니다.

공식문서에서 인증 해시를 생성하는 방법을 다음과 같이 설명하고 있습니다.

💡 To generate the authentication string, follow these steps:

  • Concatenate the websocket password with the salt provided by the server (password + salt)
  • Generate an SHA256 binary hash of the result and base64 encode it, known as a base64 secret.
  • Concatenate the base64 secret with the challenge sent by the server (base64_secret + challenge)
  • Generate a binary SHA256 hash of that result and base64 encode it. You now have your authentication string.

인증에 성공하면 Identify 때 구독한 이벤트 타입이 트리거 되는 즉시 이벤트 메시지를 수신할 수 있다고 합니다.

// socket.onmessage 핸들러
const handleMessage = useCallback(
    (event) => {
      const message = JSON.parse(event.data);

      console.log(message, '<<< onmessage');

      if (message.op === 0) {
        // 인증 요청
        requestAuth(message);
      }
      ...
      
      
const requestAuth = useCallback((message) => {
    // 인증 해시 계산
    const salt = message.d.authentication.salt;
    const challenge = message.d.authentication.challenge;

    const passwordSaltHash = CryptoJS.SHA256(PASSWORD + salt).toString(CryptoJS.enc.Base64);
    const authentication = CryptoJS.SHA256(passwordSaltHash + challenge).toString(
      CryptoJS.enc.Base64
    );

    // Identify
    const messageTemplate = {
      op: 1,
      d: {
        rpcVersion: 1,
        authentication,
        // 구독할 이벤트 종류 (음소거, 장면 전환, 스트리밍 시작/중지)
        eventSubscriptions: (1 << 3) | (1 << 4) | (1 << 6),
      },
    };

    // 인증 요청
    ws.current.send(JSON.stringify(messageTemplate));
  }, []);

저는 CryptoJS 라이브러리를 통해 인증 해시를 계산해주었습니다.

PBKDF2, SHA-256 등 여러가지 해시 함수를 사용할 수 있습니다.

3. 이벤트 구독

인증에 성공했습니다! 🥳
이제 첫 Identify 메시지 전송 시 함께 전달한 eventSubscriptions에 할당한 이벤트가 트리거되면 웹소켓을 통해 이벤트를 수신할 수 있습니다.

EventSubscription::Inputs
Subscription value to receive events in the Inputs category.

  • Identifier Value: (1 << 3)
  • Latest Supported RPC Version: 1
  • Added in v5.0.0

음소거

// 마이크 음소거 (1 << 3 이벤트 구독 시 수신 가능)
{
    "d": {
        "eventData": {
            "inputMuted": true,
            "inputName": "마이크/Aux",
            "inputUuid": "1d798455-d0a2-4b65-b74f-206b360f62e6"
        },
        "eventIntent": 8,
        "eventType": "InputMuteStateChanged"
    },
    "op": 5
}

// 미디어 소스 음소거
{
    "d": {
        "eventData": {
            "inputMuted": true,
            "inputName": "미디어 소스",
            "inputUuid": "6281c743-0a97-4a79-9ff6-9a7df96f5db4"
        },
        "eventIntent": 8,
        "eventType": "InputMuteStateChanged"
    },
    "op": 5
}

이제 DJ가 OBS 스튜디오에서 음소거 버튼을 누르면 웹소켓으로 이벤트를 수신하여 웹 페이지에서 Toast 메시지를 띄우는 등, 유저에게 다양한 정보를 제공할 수 있게 되었습니다.

음소거 외에도 장면 전환, 녹화 상태 변경, 스트리밍 시작/중지 같은 다양한 기능을 활용하여 더욱 다이나믹한 방송 경험을 만들 수 있습니다. 🚀

어떤 이벤트를 구독할 수 있는지 EventSubscription 섹션을 확인해보세요.

4. 다른 이벤트 구독

Reidentify (OpCode 3)
Sent from: Identified client
Sent to: obs-websocket
Description: Sent at any time after initial identification to update the provided session parameters.

한가지 불편한 점은 최초 Identify 시 지정한 이벤트만 수신할 수 있으며, 추가 이벤트를 구독하려면 ReIdentify를 사용해야 한다는 것입니다.

처음부터 모든 이벤트를 구독하면 불필요한 이벤트까지 웹소켓을 통해 전달되므로 네트워크 트래픽이 증가합니다. 필요한 이벤트만 구독하는 것이 성능 최적화에 유리하겠네요.

다른 이벤트를 구독하려면 ReIdentify 메시지를 전송해야 합니다.

{
  "op": 3,
  "d": {
    // 기존 이벤트 + 새로운 이벤트 구독
    "eventSubscriptions": (1 << 3) | (1 << 6)  
  }
}

특정 인터랙션이 트리거 되었을 때만 ReIdentify를 호출하여 서버 요청을 줄이는 방향이 좋을 것 같습니다.

5. 웹소켓 연결 확인 및 자동 재연결 처리

지금까지 웹소켓을 연결하고 OBS 스튜디오 내에서 발생하는 이벤트를 탐지할 수 있는 로직을 살펴보았습니다.

한편, 사용자가 OBS에서 웹소켓을 사용하지 않으면 이벤트 구독을 할 수가 없습니다.
유저가 OBS 웹소켓 설정을 활성화 했는지 알아내려면 웹소켓 연결 시 연결 실패 이벤트(onerror, onclose)를 활용하는 것이 좋습니다.

const ws = new WebSocket("ws://localhost:4455");

ws.onerror = () => {
  console.error("WebSocket 연결 실패: OBS 웹소켓이 활성화되지 않았을 가능성이 높습니다.");
  alert("OBS 설정에서 '웹소켓 사용'을 활성화해야 합니다.");
};

ws.onclose = (event) => {
  console.warn("WebSocket 연결이 닫혔습니다. (코드:", event.code, ")");
  if (event.code === 1006) {
    alert("OBS 설정에서 '웹소켓 사용'을 활성화해야 합니다.");
  }
};

OBS 설정하는 방법을 가이드 문서로 제공하여 사용자가 직접 활성화할 수 있도록 안내하는 방향이 가장 좋을 것 같습니다.

웹소켓 설정이 활성화 되지 않은 이슈 외에도, 보안이나 네트워크 문제로 인해 웹소켓이 끊어질 수도 있습니다. 이때 자동 재연결 기능을 제공할 수 있습니다.

  1. 웹소켓이 onclose 또는 onerror 발생 시 일정 시간 후 재연결
  2. 최대 재시도 횟수를 설정하여 무한 루프 방지
  3. 지수 백오프(Exponential Backoff)를 사용하여 점진적 재연결 간격 증가
  4. OBS 실행 여부를 체크 후 재연결 시도

예시 코드

const RECONNECT_INTERVAL_BASE = 2000; // 2초 (기본 재연결 간격)
const MAX_RECONNECT_ATTEMPTS = 5; // 최대 재시도 횟수
let reconnectAttempts = 0; // 현재 재시도 횟수

const connectWebSocket = () => {
  if (ws.current && ws.current.readyState !== WebSocket.CLOSED) {
    return; // 이미 연결 중이면 중복 연결 방지
  }

  console.log("WebSocket 연결 시도...");
  ws.current = new WebSocket(SOCKET_URL);

  ws.current.onopen = () => {
    console.log("✅ WebSocket 연결 성공");
    reconnectAttempts = 0; // 재연결 카운트 초기화
  };

  ws.current.onmessage = (event) => handleMessage(event);

  ws.current.onerror = (error) => {
    console.error("❌ WebSocket 오류 발생:", error);
  };

  ws.current.onclose = (event) => {
    console.warn(`⚠️ WebSocket 연결 종료 (코드: ${event.code})`);

    if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
      const reconnectDelay = RECONNECT_INTERVAL_BASE * (2 ** reconnectAttempts); // 지수 백오프 적용
      console.log(`🔄 ${reconnectDelay / 1000}초 후 재연결 시도...`);
      
      setTimeout(() => {
        reconnectAttempts++;
        connectWebSocket();
      }, reconnectDelay);
    } else {
      console.error("🚨 최대 재연결 횟수 초과. 재연결 중단.");
    }
  };
};

// useEffect에서 실행
useEffect(() => {
  connectWebSocket();

  return () => {
    if (ws.current) {
      ws.current.close();
    }
  };
}, [handleMessage]);

자동 재연결 로직을 도입하면, OBS가 재시작되거나 네트워크가 일시적으로 끊겼을 때도 웹소켓이 지속적으로 연결을 유지하도록 하여 더욱 안정적인 서비스를 만들 수 있겠습니다.

앞으로 더 많은 기능을 추가하면서 스푼웹에서의 라이브 방송이 더욱 활발해졌으면 좋겠습니다 🚀

profile
아름다운 세상

0개의 댓글