見出し画像

【Unity】OBSをWebSocketで外部から制御する

フリーの配信ソフトウェアとして知られているOBS StudioをUnityから制御してみます。OBSには外部から制御を行うための仕組みとしてWebSocketサーバが組み込まれています。このWebSocketサーバとUnityでやり取りすることで、ソースやシーンの切り替え、再生、停止など様々な操作を外部制御できるようになります。


環境

Unity 2022.3.34f1 / OBS 30.2.3

1. 準備

OBSのWebSocketサーバとはJSONフォーマットを使ってデータをやり取りを行います。まずはUnityでJSONを扱いやすくするためのライブラリとしてNewtonsoft.Jsonを使用します。記述方法は変わりますが標準で搭載されているJsonUtilityを使用しても構いません。今回はNewtonsoft.Jsonで解説を進めます。

導入手順

  1. Unityエディタのメニューの[Window] > [Package Manager]を選択

  2. 左上の「+」ボタン > [Add package from git URL…」を選択

  3. URL欄に「com.unity.nuget.newtonsoft-json」を入力して[Add]を押す

これでNewtonsoft.Jsonがプロジェクト内にインポートされます。

2. スクリプト

今回の例ではOBSに設定した動画ソースの再生・一時停止・停止・リスタートをUnityから操作してみます。以下のソースコードを適当なゲームオブジェクトにアタッチします。

using System;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;
using UnityEngine;
using WebSocketSharp;

// OBS Document
// https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md
public class Controller : MonoBehaviour {
    
    private const string SERVER_PASS = "******";
    
    private WebSocket m_Socket;

    private const string OBS_WEBSOCKET_MEDIA_INPUT_ACTION_RESTART = "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_RESTART";
    private const string OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PLAY = "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PLAY";
    private const string OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PAUSE = "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PAUSE";
    private const string OBS_WEBSOCKET_MEDIA_INPUT_ACTION_STOP = "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_STOP";
    
    private class BaseMessage {
        [JsonProperty("op")] public virtual int Op { get; set; }
    }
    
    private class MessageHelloDataKeys {
        [JsonProperty("obsWebSocketVersion")] public string ObsWebSocketVersion { get; set; }
        [JsonProperty("rpcVersion")] public int RPCVersion { get; set; }
        [JsonProperty("authentication")] public Authentication Auth { get; set; }
    }
    
    private class Authentication {
        [JsonProperty("challenge")] public string Challenge { get; set; }
        [JsonProperty("salt")] public string Salt { get; set; }
    }
    
    private class MessageHello : BaseMessage {
        [JsonProperty("op")]
        public override int Op { get; set; }
        
        [JsonProperty("d")]
        public MessageHelloDataKeys D { get; } = new();
    }

    private class MessageIdentifyDataKeys {
        [JsonProperty("rpcVersion")]
        public int RPCVersion { get; } = 1;
        
        [JsonProperty("authentication")]
        public string Auth { get; set; }
    }
    
    private class MessageIdentify : BaseMessage {
        [JsonProperty("op")]
        public override int Op { get; set; } = 1;
        
        [JsonProperty("d")]
        public MessageIdentifyDataKeys D { get; set; } = new();
        
        public MessageIdentify() { }
        
        public MessageIdentify(string auth) {
            D.Auth = auth;
        }
    }
    
    private class MessageRequest : BaseMessage {
        [JsonProperty("op")]
        public override int Op { get; set; } = 6;
        
        [JsonProperty("d")]
        public MessageRequestDataKeys D { get; set; }
        public MessageRequest(string requestType, string requestId, RequestData requestData) {
            D = new MessageRequestDataKeys(requestType, requestId, requestData);
        }
    }
    
    private class MessageRequestDataKeys {
        [JsonProperty("requestType")] 
        public string RequestType { get; }
        
        [JsonProperty("requestId")]
        public string RequestId { get; }
        
        [JsonProperty("requestData")]
        public RequestData RequestData { get; }
        
        public MessageRequestDataKeys(string requestType, string requestId, RequestData requestData) {
            RequestType = requestType;
            RequestId = requestId;
            RequestData = requestData;
        }
    }
    
    private class RequestData {
        [JsonProperty("inputName")]
        public string InputName { get; }
        [JsonProperty("mediaAction")]
        public string MediaAction { get; }

        public RequestData(string inputName, string mediaAction) {
            InputName = inputName;
            MediaAction = mediaAction;
        }
    }
    
    private void Start() {
        m_Socket = new WebSocket("ws://localhost:4455");

        m_Socket.OnOpen += (_, _) => {
            Debug.Log("WebSocket 接続成功");
        };

        m_Socket.OnMessage += (_, e) => {
            Debug.Log("OnMessage");
            Debug.Log($"Res: {e.Data}");
            
            var res = JsonConvert.DeserializeObject<BaseMessage>(e.Data);

            if (res.Op == 0) {
                var hello = JsonConvert.DeserializeObject<MessageHello>(e.Data);
                if (hello == null) return;
                
                Debug.Log($"Receive Hello: {hello.D.ObsWebSocketVersion}, {hello.D.RPCVersion}");

                if (hello.D.Auth == null) {
                    m_Socket.Send(CreateMessage(new MessageIdentify()));
                } else {
                    Debug.Log($"Auth: Challenge: {hello.D.Auth.Challenge}, Salt: {hello.D.Auth.Salt}");
                    var auth = CreateAuth(hello.D.Auth);
                    m_Socket.Send(CreateMessage(new MessageIdentify(auth)));
                }
            }
        };

        m_Socket.OnError += (_, e) => { Debug.Log("エラー: " + e.Message); };
        m_Socket.OnClose += (_, e) => { Debug.Log($"WebSocket 切断: {e}"); };
        m_Socket.Connect();
    }
    
    private string CreateMessage(BaseMessage message) {
        var json = JsonConvert.SerializeObject(message);
        Debug.Log(json);
        return json;
    }
    
    private string CreateInputAction(string inputName, string mediaAction) {
        var requestData = new RequestData(inputName, mediaAction);
        var request = new MessageRequest("TriggerMediaInputAction", "999", requestData);
        return CreateMessage(request);
    }
    
    private void Update() {
        if (Input.GetKeyDown(KeyCode.A)) {
            m_Socket.Send(CreateInputAction("M12", OBS_WEBSOCKET_MEDIA_INPUT_ACTION_RESTART));
        }
        
        if (Input.GetKeyDown(KeyCode.S)) {
            m_Socket.Send(CreateInputAction("M12", OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PLAY));
        }
        
        if (Input.GetKeyDown(KeyCode.D)) {
            m_Socket.Send(CreateInputAction("M12", OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PAUSE));
        }
        
        if (Input.GetKeyDown(KeyCode.F)) {
            m_Socket.Send(CreateInputAction("M12", OBS_WEBSOCKET_MEDIA_INPUT_ACTION_STOP));
        }
    }
    
    private string CreateAuth(Authentication auth) {
        string base64Secret = CreateSHA256($"{SERVER_PASS}{auth.Salt}");
        string authentication = CreateSHA256($"{base64Secret}{auth.Challenge}");
        return authentication;
    }

    private string CreateSHA256(string input) {
        byte[] passConcat = Encoding.UTF8.GetBytes(input);
        using var sha256 = SHA256.Create();
        byte[] sha256Hash = sha256.ComputeHash(passConcat);
        return CreateBase64Secret(sha256Hash);
    }

    private string CreateBase64Secret(byte[] input) {
       return Convert.ToBase64String(input);
    }
    
    private void OnDestroy() {
        m_Socket.Close();
        m_Socket = null;
    }
}

少しコードが長くなっていますが、解説用のサンプルとしてJSONのデシリアライズクラスも一緒に記述しています。本格的に使用する場合は、ファイルを分けると良いでしょう。コード内の主要な部分については後ほど解説します。

またOBSのWebSocketサーバを介した制御プロトコルはGitHubで公開されています。他にも具体的な処理の方法やメッセージの種類などが記載されていますので、目を通しておくと良いと思います。

https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md

3. OBS側の設定

メニューの[ツール] > [WebSocketサーバー設定]を選択

WebSocketサーバー設定の表示

プラグイン設定で「WebSocketサーバーを有効にする」にチェックを入れます。サーバ設定では「認証を有効ににする」のチェックを外します。認証を有効にする場合は、Unity側のスクリプトのSERVER_PASSに同じ値を設定します。

設定が完了したら[適用]または[OK]ボタンをクリックします。

WebSocketサーバー設定

次にOBS内で再生制御を行うためのソースを追加します。ソースの[+]からメディアソースを選択します。

ソースの追加

「新規作成」の入力欄に任意のソース名を入力て[OK]ボタンをクリックします。スクリプトからはこの名前を指定してソースを制御できるようになります。今回のサンプルスクリプトでは「M12」としました。

ソースの作成

次にプロパティウィンドウが表示されるので、[参照]ボタンをクリックして再生する動画ファイルを選択します。ソースが読み込まれたら表示される位置を調整します。

ソースのプロパティ

これでOBS側の設定は完了です。

4. 動作チェック

Unityを実行してスクリプト内で定義した以下のキーでOBS内のソースの再生制御ができることを確認します。

OBS内のソース制御

A:リスタート
S:再生スタート
D:一時停止
F:停止

5. コード解説

ここからは先ほどのスクリプトについて解説します。まず、重要なポイントとしてUnityからOBSのWebSocketサーバへ接続すると以下のJSONが返ってきます。

{
    "op": 0,
    "d": {
        "obsWebSocketVersion": "5.1.0",
        "rpcVersion": 1
    }
}

これはHello(OpCode 0)と呼ばれるもので、このメッセージに対してクライアント(Unity)から応答することで、制御コードを受け付けてくれるようになります。OBS側で認証を有効にしている場合は、認証に必要なchallenge値とsalt値が追加されて返ってきます。

認証ありの場合

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

Helloメッセージを受信したらIdentify (OpCode 1)メッセージとして以下のJSONフォーマットで返信します。(認証が必要な場合はauthenticationを追加)

{
    "op": 1,
    "d": {
        "rpcVersion": 1
    }
}

Unity側がOBSにクライアントとして認識されたあとは、Request (OpCode 6)で制御コマンドをOBS側に送信します。JSONフォーマットの例としては以下のようになります。

{
    "op": 6,
    "d": {
        "requestType": "TriggerMediaInputAction",
        "requestId": "999",
        "requestData": {
            "inputName": "M12",
            "mediaAction": "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_RESTART"
        }
    }
}

今回の例では、OBS側からみて入力されたコマンドを元にソースを制御したいため、requestTypeにはTriggerMediaInputActionを指定します。requestDataのinputNameには制御したい対象のソース名、mediaActionは行わせたいアクションを指定します。mediaActionには以下のコマンドが用意されています。

6. おわりに

今回はクライアント側(Unity)から操作を行う方法を解説しましたが、公式ドキュメントにはOBSのイベントを受け取ったり、状態を取得するコマンドも用意されていることがわかります。

Youtube等でのネット配信が数多く行われるようになった昨今では、配信ツールであるOBSの需要も高まりつつあります。外部からOBSの制御ができるようになるとワンオペがやりやすくなったり、配信トラブルの低減など色々工夫や面白いこともできるようになるかもしれません。🌱

7. 参考リンク

https://github.com/obsproject/obs-websocket
https://www.newtonsoft.com/json/help/html/Introduction.htm


この記事が気に入ったらサポートをしてみませんか?